soc operations

Performing Alert Triage with Elastic SIEM

Perform systematic alert triage in Elastic Security SIEM to rapidly classify, prioritize, and investigate security alerts for SOC operations.

alert-triagedetectionelasticelastic-securityesqlkibanasiemsoc
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Alert triage in Elastic Security is the systematic process of reviewing, classifying, and prioritizing security alerts to determine which represent genuine threats. Elastic's AI-driven Attack Discovery feature can triage hundreds of alerts down to discrete attack chains, but skilled analyst triage remains essential. A structured triage workflow typically takes 5-10 minutes per alert cluster using Elastic's built-in tools.

When to Use

  • When conducting security assessments that involve performing alert triage with elastic siem
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Elastic Security deployed (version 8.x or later)
  • Elastic Agent or Beats configured for endpoint and network data collection
  • Detection rules enabled and generating alerts
  • Elastic Common Schema (ECS) compliance across data sources
  • Analyst access to Kibana Security app with appropriate privileges

Alert Triage Workflow

Step 1: Initial Alert Assessment (2 minutes)

When viewing an alert in Elastic Security, review the alert details panel:

Alert Details Panel:
- Rule Name and Description
- Severity and Risk Score
- MITRE ATT&CK Mapping
- Host and User Context
- Process Tree (for endpoint alerts)
- Timeline of related events

Key Fields to Examine First

Field Purpose ECS Field
Rule severity Initial priority assessment kibana.alert.severity
Risk score Quantified threat level kibana.alert.risk_score
Host name Affected system host.name
User name Affected identity user.name
Process name Executing process process.name
Source IP Origin of activity source.ip
Destination IP Target of activity destination.ip
MITRE tactic Attack stage threat.tactic.name

Step 2: Context Gathering (3 minutes)

Query Related Events with ES|QL

FROM logs-endpoint.events.*
| WHERE host.name == "affected-host" AND @timestamp > NOW() - 1 HOUR
| STATS count = COUNT(*) BY event.category, event.action
| SORT count DESC

Find All Activity from Suspicious User

FROM logs-*
| WHERE user.name == "suspicious-user" AND @timestamp > NOW() - 24 HOURS
| STATS count = COUNT(*), unique_hosts = COUNT_DISTINCT(host.name) BY event.category
| SORT count DESC

Check for Related Alerts from Same Source

FROM .alerts-security.alerts-default
| WHERE source.ip == "10.0.0.50" AND @timestamp > NOW() - 24 HOURS
| STATS alert_count = COUNT(*) BY kibana.alert.rule.name, kibana.alert.severity
| SORT alert_count DESC

Investigate Lateral Movement from Same IP

FROM logs-system.auth-*
| WHERE source.ip == "10.0.0.50" AND event.outcome == "success"
| STATS login_count = COUNT(*), hosts = COUNT_DISTINCT(host.name) BY user.name
| WHERE hosts > 3

Step 3: Threat Intelligence Enrichment (2 minutes)

Check indicators against threat intelligence:

FROM logs-ti_*
| WHERE threat.indicator.ip == "203.0.113.50"
| KEEP threat.indicator.type, threat.indicator.provider, threat.indicator.confidence, threat.feed.name

Check File Hash Against Known Threats

FROM logs-endpoint.events.file-*
| WHERE file.hash.sha256 == "abc123..."
| STATS occurrences = COUNT(*) BY host.name, file.path, user.name

Step 4: Classification Decision (2 minutes)

Classification Criteria Action
True Positive Confirmed malicious activity Escalate to incident, begin containment
Benign True Positive Expected behavior matching rule Document in alert notes, acknowledge
False Positive Rule triggered on benign activity Mark as false positive, create tuning task
Needs Investigation Insufficient data for determination Assign for deeper investigation

Step 5: Documentation and Escalation (1 minute)

For each triaged alert, document:

  • Classification decision with rationale
  • Evidence artifacts examined
  • Related alerts or investigations
  • Recommended next steps

Detection Rules for Triage

Pre-Built Detection Rules

Elastic Security includes 1000+ pre-built detection rules organized by:

  • MITRE ATT&CK Tactic: Initial Access, Execution, Persistence, etc.
  • Platform: Windows, Linux, macOS, Cloud
  • Data Source: Endpoint, Network, Cloud, Identity

Custom Alert Correlation Rule

{
  "name": "Multiple Failed Logins Followed by Success",
  "type": "threshold",
  "query": "event.category:authentication AND event.outcome:failure",
  "threshold": {
    "field": ["source.ip", "user.name"],
    "value": 5,
    "cardinality": [
      {
        "field": "user.name",
        "value": 3
      }
    ]
  },
  "severity": "high",
  "risk_score": 73,
  "threat": [
    {
      "framework": "MITRE ATT&CK",
      "tactic": {
        "id": "TA0006",
        "name": "Credential Access"
      },
      "technique": [
        {
          "id": "T1110",
          "name": "Brute Force"
        }
      ]
    }
  ]
}

AI-Assisted Triage

Elastic AI Assistant Integration

  1. Open alert in Elastic Security
  2. Click AI Assistant panel
  3. Use quick prompts:
    • "Summarize this alert" - Get initial assessment
    • "Generate ES|QL query to find related activity" - Expand investigation
    • "What are the recommended response actions?" - Get playbook guidance
    • "Is this likely a false positive?" - Get AI confidence assessment

Attack Discovery

Elastic's Attack Discovery automatically:

  • Groups related alerts into attack chains
  • Maps alerts to MITRE ATT&CK kill chain stages
  • Filters false positives using ML models
  • Prioritizes based on business impact
  • Provides narrative summary of the attack

Triage Prioritization Matrix

Risk Score Severity Asset Criticality Response SLA
90-100 Critical High 15 minutes
70-89 High High 30 minutes
70-89 High Medium 1 hour
50-69 Medium Any 4 hours
21-49 Low Any 8 hours
1-20 Informational Any 24 hours

Triage Metrics and KPIs

Metric Target Measurement
Mean Time to Triage (MTTT) < 10 minutes Time from alert creation to classification
False Positive Rate < 30% False positives / total alerts
Escalation Rate 10-20% Escalated alerts / total alerts
Alert Coverage > 80% Triaged alerts / generated alerts per shift
Reclassification Rate < 5% Changed classifications / total classified

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.8 KB

Alert Triage with Elastic SIEM - API Reference

elasticsearch-py Client

Connection

from elasticsearch import Elasticsearch
es = Elasticsearch(
    hosts=["https://elastic:9200"],
    api_key="base64-api-key",
    verify_certs=True
)

SIEM Signals Index

Elastic Security stores alerts in .siem-signals-<space>-* indices.

Querying Alerts

Search Open Alerts

es.search(
    index=".siem-signals-*",
    query={"bool": {"must": [
        {"range": {"@timestamp": {"gte": "now-24h"}}},
        {"term": {"signal.status": "open"}}
    ]}},
    sort=[{"@timestamp": {"order": "desc"}}],
    size=500
)

Alert Fields

Field Path Description
Rule name signal.rule.name Detection rule that triggered
Rule ID signal.rule.id Unique rule identifier
Severity signal.rule.severity critical, high, medium, low
Risk score signal.rule.risk_score 0-100 numeric score
Status signal.status open, acknowledged, closed
Source IP source.ip Alert source address
Destination IP destination.ip Alert destination address
User user.name Associated username
Host host.name Affected hostname
Process process.name Triggering process

Aggregations

es.search(
    index=".siem-signals-*",
    query={"bool": {"must": [...]}},
    aggs={
        "by_severity": {"terms": {"field": "signal.rule.severity", "size": 10}},
        "by_rule": {"terms": {"field": "signal.rule.name.keyword", "size": 20}},
        "by_host": {"terms": {"field": "host.name.keyword", "size": 20}}
    },
    size=0
)

Alert Status Management

Update Alert Status

es.update(
    index=".siem-signals-default-000001",
    id="alert_doc_id",
    body={"doc": {"signal": {"status": "closed"}}}
)

Triage Prioritization

Severity Priority

  1. Critical (risk score 90-100)
  2. High (risk score 70-89)
  3. Medium (risk score 40-69)
  4. Low (risk score 0-39)

Alert Clustering

Alerts from the same host within a time window are grouped as potential incidents. Three or more alerts from the same host suggest a multi-stage attack.

Elastic Security API

List Detection Rules

GET /api/detection_engine/rules/_find?per_page=100

Get Rule Execution Status

GET /api/detection_engine/rules/_find_statuses

Output Schema

{
  "report": "elastic_siem_alert_triage",
  "total_open_alerts": 45,
  "severity_summary": {"critical": 3, "high": 12, "medium": 20, "low": 10},
  "alert_clusters": [{"host": "web01", "alert_count": 5, "max_severity": "high"}],
  "aggregations": {"by_severity": [{"key": "high", "count": 12}]}
}

CLI Usage

python agent.py --host https://elastic:9200 --api-key "key" --hours 24 --output report.json
standards.md1.7 KB

Standards and References - Alert Triage with Elastic SIEM

Elastic Common Schema (ECS)

ECS is a standardized field naming convention for Elasticsearch data. All Elastic Security detections and triage workflows rely on ECS compliance.

Key ECS Field Categories for Triage

Category Fields Usage
Base @timestamp, message, tags Event timing and classification
Agent agent.name, agent.type Data source identification
Host host.name, host.ip, host.os Affected system context
User user.name, user.domain Identity attribution
Process process.name, process.pid, process.command_line Execution context
Network source.ip, destination.ip, destination.port Network activity
File file.name, file.hash.sha256, file.path File-related events
Threat threat.tactic.name, threat.technique.id MITRE ATT&CK mapping

MITRE ATT&CK Integration

Elastic Security maps detection rules and alerts to MITRE ATT&CK tactics and techniques, providing a common taxonomy for triage prioritization.

NIST SP 800-61 Rev 2

Triage aligns with NIST incident handling phases:

  • Detection and Analysis (triage is the core of this phase)
  • Prioritization based on functional impact, information impact, and recoverability

SOC Maturity Model

Triage Capability Levels

Level Capability
Level 1 Manual review of individual alerts
Level 2 Grouped alert triage with correlation
Level 3 AI-assisted triage with automated enrichment
Level 4 Automated classification with human oversight
Level 5 Fully autonomous triage with exception-based review
workflows.md1.6 KB

Workflows - Alert Triage with Elastic SIEM

5-Step Rapid Triage Framework

1. Alert Reception (30 seconds)
   - Review alert title, severity, risk score
   - Check MITRE ATT&CK mapping
   |
   v
2. Context Assessment (2 minutes)
   - Examine affected host and user
   - Check asset criticality
   - Review process tree for endpoint alerts
   |
   v
3. Intelligence Enrichment (2 minutes)
   - Check threat intelligence feeds
   - Query for related alerts (same source/user)
   - Search for known IOCs
   |
   v
4. Classification (1 minute)
   - True Positive / False Positive / Needs Investigation
   - Assign confidence level
   |
   v
5. Action (2 minutes)
   - Document findings in alert notes
   - Escalate or close with rationale
   - Create tuning task if false positive

Alert Grouping Strategy

Smart Grouping Criteria

  • Time window: Group alerts within 15-minute windows
  • Entity: Group by affected host or user
  • Kill chain stage: Group by MITRE ATT&CK tactic
  • Source: Group by originating IP or detection rule

Group Triage Process

  1. Sort alert groups by highest severity member
  2. Triage group as single unit when correlated
  3. Escalate entire group if attack chain detected
  4. Close group if false positive pattern identified

Shift-Based Triage Queue Management

Queue Priority Alert Criteria Analyst Tier
Immediate Critical severity, critical assets Tier 2+
High High severity or multiple related alerts Tier 1/2
Standard Medium severity, standard assets Tier 1
Low Low/info severity, non-critical Tier 1 (batch review)

Scripts 2

agent.py6.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Alert Triage with Elastic SIEM agent - queries Elasticsearch SIEM signals
index to retrieve, prioritize, and triage security alerts using
elasticsearch-py client."""

import argparse
import json
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path

try:
    from elasticsearch import Elasticsearch
except ImportError:
    print("Install elasticsearch: pip install elasticsearch", file=sys.stderr)
    sys.exit(1)


SIEM_SIGNALS_INDEX = ".siem-signals-*"

SEVERITY_PRIORITY = {"critical": 1, "high": 2, "medium": 3, "low": 4}


def create_client(host: str, api_key: str = None, username: str = None,
                  password: str = None, verify_certs: bool = True) -> Elasticsearch:
    """Create Elasticsearch client."""
    kwargs = {"hosts": [host], "verify_certs": verify_certs}
    if api_key:
        kwargs["api_key"] = api_key
    elif username and password:
        kwargs["basic_auth"] = (username, password)
    return Elasticsearch(**kwargs)


def get_open_alerts(es: Elasticsearch, hours_back: int = 24,
                    severity: list[str] = None, size: int = 500) -> list[dict]:
    """Retrieve open SIEM alerts from the signals index."""
    must_clauses = [
        {"range": {"@timestamp": {"gte": f"now-{hours_back}h", "lte": "now"}}},
        {"term": {"signal.status": "open"}},
    ]
    if severity:
        must_clauses.append({"terms": {"signal.rule.severity": severity}})

    query = {"bool": {"must": must_clauses}}
    response = es.search(index=SIEM_SIGNALS_INDEX, query=query, size=size,
                         sort=[{"@timestamp": {"order": "desc"}}])
    alerts = []
    for hit in response["hits"]["hits"]:
        src = hit["_source"]
        signal = src.get("signal", {})
        rule = signal.get("rule", {})
        alerts.append({
            "alert_id": hit["_id"],
            "timestamp": src.get("@timestamp", ""),
            "rule_name": rule.get("name", ""),
            "rule_id": rule.get("id", ""),
            "severity": rule.get("severity", "unknown"),
            "risk_score": rule.get("risk_score", 0),
            "status": signal.get("status", ""),
            "source_ip": src.get("source", {}).get("ip", ""),
            "destination_ip": src.get("destination", {}).get("ip", ""),
            "user": src.get("user", {}).get("name", ""),
            "host": src.get("host", {}).get("name", ""),
            "process": src.get("process", {}).get("name", ""),
        })
    return alerts


def get_alert_aggregations(es: Elasticsearch, hours_back: int = 24) -> dict:
    """Aggregate alerts by rule, severity, and host."""
    query = {
        "bool": {
            "must": [
                {"range": {"@timestamp": {"gte": f"now-{hours_back}h"}}},
                {"term": {"signal.status": "open"}},
            ]
        }
    }
    aggs = {
        "by_severity": {"terms": {"field": "signal.rule.severity", "size": 10}},
        "by_rule": {"terms": {"field": "signal.rule.name.keyword", "size": 20}},
        "by_host": {"terms": {"field": "host.name.keyword", "size": 20}},
        "by_user": {"terms": {"field": "user.name.keyword", "size": 20}},
    }
    response = es.search(index=SIEM_SIGNALS_INDEX, query=query, aggs=aggs, size=0)
    result = {}
    for agg_name, agg_data in response.get("aggregations", {}).items():
        result[agg_name] = [
            {"key": bucket["key"], "count": bucket["doc_count"]}
            for bucket in agg_data.get("buckets", [])
        ]
    return result


def prioritize_alerts(alerts: list[dict]) -> list[dict]:
    """Sort and prioritize alerts by severity and risk score."""
    return sorted(alerts, key=lambda a: (
        SEVERITY_PRIORITY.get(a.get("severity", "low"), 5),
        -a.get("risk_score", 0)
    ))


def identify_alert_clusters(alerts: list[dict]) -> list[dict]:
    """Group related alerts that may represent a single incident."""
    clusters = []
    by_host = {}
    for alert in alerts:
        host = alert.get("host", "unknown")
        if host not in by_host:
            by_host[host] = []
        by_host[host].append(alert)

    for host, host_alerts in by_host.items():
        if len(host_alerts) >= 3:
            rules = list(set(a["rule_name"] for a in host_alerts))
            max_severity = min(host_alerts, key=lambda a: SEVERITY_PRIORITY.get(a.get("severity", "low"), 5))
            clusters.append({
                "host": host,
                "alert_count": len(host_alerts),
                "unique_rules": len(rules),
                "rules": rules[:10],
                "max_severity": max_severity["severity"],
                "recommendation": "Investigate as potential incident - multiple alerts on same host",
            })
    return clusters


def generate_report(host: str, api_key: str = None, username: str = None,
                    password: str = None, hours_back: int = 24,
                    severity: list[str] = None) -> dict:
    """Run alert triage and build consolidated report."""
    es = create_client(host, api_key, username, password)
    alerts = get_open_alerts(es, hours_back, severity)
    prioritized = prioritize_alerts(alerts)
    aggregations = get_alert_aggregations(es, hours_back)
    clusters = identify_alert_clusters(alerts)

    severity_counts = Counter(a["severity"] for a in alerts)
    return {
        "report": "elastic_siem_alert_triage",
        "generated_at": datetime.utcnow().isoformat() + "Z",
        "time_window_hours": hours_back,
        "total_open_alerts": len(alerts),
        "severity_summary": dict(severity_counts),
        "alert_clusters": clusters,
        "aggregations": aggregations,
        "prioritized_alerts": prioritized[:50],
    }


def main():
    parser = argparse.ArgumentParser(description="Elastic SIEM Alert Triage Agent")
    parser.add_argument("--host", required=True, help="Elasticsearch URL")
    parser.add_argument("--api-key", help="Elasticsearch API key")
    parser.add_argument("--username", help="Elasticsearch username")
    parser.add_argument("--password", help="Elasticsearch password")
    parser.add_argument("--hours", type=int, default=24, help="Hours to look back (default: 24)")
    parser.add_argument("--severity", nargs="+", help="Filter by severity levels")
    parser.add_argument("--output", help="Output JSON file path")
    args = parser.parse_args()

    report = generate_report(args.host, args.api_key, args.username,
                             args.password, args.hours, args.severity)
    output = json.dumps(report, indent=2)
    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)


if __name__ == "__main__":
    main()
process.py11.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Elastic SIEM Alert Triage Automation

Provides alert triage scoring, classification assistance,
and triage workflow management for Elastic Security alerts.
"""

import json
from datetime import datetime, timedelta
from typing import Optional


SEVERITY_WEIGHTS = {
    "critical": 100,
    "high": 75,
    "medium": 50,
    "low": 25,
    "informational": 10,
}

ASSET_CRITICALITY = {
    "critical": 1.5,
    "high": 1.3,
    "medium": 1.0,
    "low": 0.7,
}

MITRE_TACTIC_PRIORITY = {
    "Impact": 95,
    "Exfiltration": 90,
    "Lateral Movement": 85,
    "Credential Access": 80,
    "Command and Control": 75,
    "Defense Evasion": 70,
    "Privilege Escalation": 65,
    "Persistence": 60,
    "Execution": 55,
    "Collection": 50,
    "Discovery": 40,
    "Initial Access": 35,
    "Reconnaissance": 20,
    "Resource Development": 15,
}


class TriageAlert:
    """Represents a security alert for triage processing."""

    def __init__(
        self,
        alert_id: str,
        rule_name: str,
        severity: str,
        risk_score: int,
        host_name: str,
        user_name: str,
        source_ip: str,
        mitre_tactic: str,
        mitre_technique: str,
        timestamp: str,
        asset_criticality: str = "medium",
        related_alert_count: int = 0,
        threat_intel_match: bool = False,
    ):
        self.alert_id = alert_id
        self.rule_name = rule_name
        self.severity = severity
        self.risk_score = risk_score
        self.host_name = host_name
        self.user_name = user_name
        self.source_ip = source_ip
        self.mitre_tactic = mitre_tactic
        self.mitre_technique = mitre_technique
        self.timestamp = timestamp
        self.asset_criticality = asset_criticality
        self.related_alert_count = related_alert_count
        self.threat_intel_match = threat_intel_match
        self.classification = None
        self.triage_notes = []
        self.triage_timestamp = None

    def calculate_triage_priority(self) -> dict:
        """Calculate composite triage priority score."""
        base_score = SEVERITY_WEIGHTS.get(self.severity, 50)
        asset_multiplier = ASSET_CRITICALITY.get(self.asset_criticality, 1.0)
        tactic_score = MITRE_TACTIC_PRIORITY.get(self.mitre_tactic, 50)

        # Boost for related alerts (potential attack chain)
        chain_boost = min(self.related_alert_count * 5, 25)

        # Boost for threat intelligence match
        ti_boost = 20 if self.threat_intel_match else 0

        composite_score = min(100, (base_score * asset_multiplier * 0.4)
                             + (tactic_score * 0.3)
                             + chain_boost
                             + ti_boost)

        if composite_score >= 85:
            priority = "P1-CRITICAL"
            sla = "15 minutes"
        elif composite_score >= 70:
            priority = "P2-HIGH"
            sla = "30 minutes"
        elif composite_score >= 50:
            priority = "P3-MEDIUM"
            sla = "4 hours"
        elif composite_score >= 30:
            priority = "P4-LOW"
            sla = "8 hours"
        else:
            priority = "P5-INFO"
            sla = "24 hours"

        return {
            "composite_score": round(composite_score, 1),
            "priority": priority,
            "response_sla": sla,
            "score_breakdown": {
                "severity_component": round(base_score * asset_multiplier * 0.4, 1),
                "tactic_component": round(tactic_score * 0.3, 1),
                "chain_boost": chain_boost,
                "threat_intel_boost": ti_boost,
            },
        }

    def classify(self, classification: str, notes: str):
        """Classify the alert after triage."""
        valid_classifications = [
            "true_positive",
            "false_positive",
            "benign_true_positive",
            "needs_investigation",
        ]
        if classification not in valid_classifications:
            raise ValueError(f"Invalid classification. Must be one of: {valid_classifications}")
        self.classification = classification
        self.triage_notes.append(notes)
        self.triage_timestamp = datetime.utcnow().isoformat()

    def to_dict(self) -> dict:
        priority = self.calculate_triage_priority()
        return {
            "alert_id": self.alert_id,
            "rule_name": self.rule_name,
            "severity": self.severity,
            "risk_score": self.risk_score,
            "host_name": self.host_name,
            "user_name": self.user_name,
            "source_ip": self.source_ip,
            "mitre_tactic": self.mitre_tactic,
            "mitre_technique": self.mitre_technique,
            "timestamp": self.timestamp,
            "triage_priority": priority,
            "classification": self.classification,
            "triage_notes": self.triage_notes,
            "triage_timestamp": self.triage_timestamp,
        }


class TriageQueue:
    """Manages a queue of alerts for SOC triage."""

    def __init__(self):
        self.alerts = []
        self.triaged = []
        self.metrics = {
            "total_received": 0,
            "total_triaged": 0,
            "true_positives": 0,
            "false_positives": 0,
            "benign_true_positives": 0,
            "pending_investigation": 0,
            "triage_times": [],
        }

    def add_alert(self, alert: TriageAlert):
        self.alerts.append(alert)
        self.metrics["total_received"] += 1

    def get_prioritized_queue(self) -> list:
        """Return alerts sorted by triage priority score (highest first)."""
        return sorted(
            self.alerts,
            key=lambda a: a.calculate_triage_priority()["composite_score"],
            reverse=True,
        )

    def triage_alert(self, alert_id: str, classification: str, notes: str, triage_duration_seconds: int):
        for i, alert in enumerate(self.alerts):
            if alert.alert_id == alert_id:
                alert.classify(classification, notes)
                self.triaged.append(alert)
                self.alerts.pop(i)
                self.metrics["total_triaged"] += 1
                self.metrics["triage_times"].append(triage_duration_seconds)
                if classification == "true_positive":
                    self.metrics["true_positives"] += 1
                elif classification == "false_positive":
                    self.metrics["false_positives"] += 1
                elif classification == "benign_true_positive":
                    self.metrics["benign_true_positives"] += 1
                elif classification == "needs_investigation":
                    self.metrics["pending_investigation"] += 1
                return alert
        return None

    def get_triage_metrics(self) -> dict:
        times = self.metrics["triage_times"]
        total = self.metrics["total_triaged"]
        return {
            "total_received": self.metrics["total_received"],
            "total_triaged": total,
            "pending": len(self.alerts),
            "true_positive_count": self.metrics["true_positives"],
            "false_positive_count": self.metrics["false_positives"],
            "false_positive_rate": round(self.metrics["false_positives"] / total * 100, 1) if total > 0 else 0,
            "mean_triage_time_seconds": round(sum(times) / len(times), 1) if times else 0,
            "max_triage_time_seconds": max(times) if times else 0,
            "min_triage_time_seconds": min(times) if times else 0,
        }


def generate_esql_queries(alert: TriageAlert) -> dict:
    """Generate ES|QL queries for investigating an alert."""
    return {
        "related_host_activity": (
            f'FROM logs-*\n'
            f'| WHERE host.name == "{alert.host_name}" '
            f'AND @timestamp > NOW() - 1 HOUR\n'
            f'| STATS count = COUNT(*) BY event.category, event.action\n'
            f'| SORT count DESC'
        ),
        "user_activity": (
            f'FROM logs-*\n'
            f'| WHERE user.name == "{alert.user_name}" '
            f'AND @timestamp > NOW() - 24 HOURS\n'
            f'| STATS count = COUNT(*), '
            f'unique_hosts = COUNT_DISTINCT(host.name) BY event.category\n'
            f'| SORT count DESC'
        ),
        "source_ip_alerts": (
            f'FROM .alerts-security.alerts-default\n'
            f'| WHERE source.ip == "{alert.source_ip}" '
            f'AND @timestamp > NOW() - 24 HOURS\n'
            f'| STATS alert_count = COUNT(*) '
            f'BY kibana.alert.rule.name, kibana.alert.severity\n'
            f'| SORT alert_count DESC'
        ),
        "network_connections": (
            f'FROM logs-endpoint.events.network-*\n'
            f'| WHERE host.name == "{alert.host_name}" '
            f'AND @timestamp > NOW() - 1 HOUR\n'
            f'| STATS conn_count = COUNT(*) BY destination.ip, destination.port\n'
            f'| SORT conn_count DESC\n'
            f'| LIMIT 20'
        ),
    }


if __name__ == "__main__":
    queue = TriageQueue()

    sample_alerts = [
        TriageAlert("ALT-001", "Multiple Failed Logins Followed by Success", "high", 73,
                    "DC01", "admin", "10.0.0.50", "Credential Access", "T1110",
                    "2025-01-15T10:30:00Z", "critical", 3, False),
        TriageAlert("ALT-002", "Suspicious PowerShell Execution", "critical", 91,
                    "WS-042", "jsmith", "10.0.1.42", "Execution", "T1059.001",
                    "2025-01-15T10:32:00Z", "high", 5, True),
        TriageAlert("ALT-003", "Unusual DNS Query Volume", "medium", 47,
                    "WS-108", "mwilson", "10.0.2.108", "Command and Control", "T1071",
                    "2025-01-15T10:35:00Z", "medium", 0, False),
        TriageAlert("ALT-004", "New Windows Service Created", "low", 21,
                    "SRV-DB01", "svc-deploy", "10.0.3.10", "Persistence", "T1543.003",
                    "2025-01-15T10:37:00Z", "low", 0, False),
    ]

    for alert in sample_alerts:
        queue.add_alert(alert)

    print("=" * 70)
    print("ELASTIC SIEM ALERT TRIAGE QUEUE")
    print("=" * 70)

    prioritized = queue.get_prioritized_queue()
    for i, alert in enumerate(prioritized, 1):
        p = alert.calculate_triage_priority()
        print(f"\n{i}. [{p['priority']}] {alert.rule_name}")
        print(f"   Score: {p['composite_score']} | SLA: {p['response_sla']}")
        print(f"   Host: {alert.host_name} | User: {alert.user_name} | Tactic: {alert.mitre_tactic}")

    # Simulate triage
    queue.triage_alert("ALT-002", "true_positive", "Confirmed Mimikatz execution via encoded PS", 180)
    queue.triage_alert("ALT-001", "true_positive", "Brute force followed by successful admin login", 300)
    queue.triage_alert("ALT-003", "false_positive", "DNS queries to known CDN - normal behavior", 120)
    queue.triage_alert("ALT-004", "benign_true_positive", "SCCM deployment creating expected service", 90)

    print(f"\n{'=' * 70}")
    print("TRIAGE METRICS")
    print("=" * 70)
    metrics = queue.get_triage_metrics()
    for key, value in metrics.items():
        print(f"  {key}: {value}")

    # Show investigation queries
    print(f"\n{'=' * 70}")
    print("ES|QL INVESTIGATION QUERIES FOR ALT-002")
    print("=" * 70)
    queries = generate_esql_queries(sample_alerts[1])
    for name, query in queries.items():
        print(f"\n--- {name} ---")
        print(query)

Assets 1

template.mdtext/markdown · 1.2 KB
Keep exploring