Skip to main content

04 - Writing SOAR Playbooks in Python: Incident Response Automation

SOAR Playbook in Python: Automazione Incident Response Un Security Orchestration, Automation and Response (SOAR) playbook e molto più di uno script: e...

What do you think about this article?

At a Glance

SOAR Playbook in Python: Automazione Incident Response Un Security Orchestration, Automation and Response (SOAR) playbook e molto più di uno script: e...

SOAR Playbook in Python: Incident Response Automation

A Security Orchestration, Automation and Response (SOAR) playbook is much more than a script: it is a formal, repeatable orchestration of actions in response to a security incident. When an alert triggers a playbook, a coordinated sequence of operations - data enrichment, malware analysis, automatic containment, notifications, and documentation - happens in seconds instead of hours, measurably reducing Mean Time to Respond (MTTR).

Recent industry data confirms the impact: Python-based implementations have achieved 2.5 minute MTTR with 92% accuracy, handling 500 incidents/hour. Cortex XSOAR, TheHive with Cortex, and platforms like Shuffle (open-source) allow implementing playbooks in Python with full access to the security ecosystem.

This article builds complete SOAR playbooks in Python for three common scenarios: phishing, endpoint malware, and brute force. For each, the entire flow is implemented: triage, enrichment, containment, escalation, and automatic documentation.

What You Will Learn

  • Architecture of a modular SOAR playbook in Python
  • Automatic enrichment: VirusTotal, Shodan, Active Directory
  • Automatic containment: endpoint isolation, IP blocking, user disabling
  • Integration with TheHive and Cortex for orchestration
  • Playbook testing and versioning
  • Patterns for human escalation with pre-populated context

Architecture of a Modular SOAR Playbook

A well-designed playbook is composed of independent functional blocks that can be reused across different playbooks. The modular structure facilitates testing, maintenance, and evolution over time.

The fundamental blocks of any SOAR playbook are:

  • Trigger: which event starts the playbook (SIEM alert, suspicious email, user report)
  • Enrichment: additional context collection (threat intel, asset info, user history)
  • Decision: automatic triage logic (false positive? escalation? immediate containment?)
  • Action: response actions (isolation, blocking, notification)
  • Documentation: ticket update, playbook log, evidence collection
  • Closure/Escalation: automatic closure or escalation to human analyst
# SOAR Playbook Base Framework
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Optional
from enum import Enum
import logging

class PlaybookStatus(Enum):
    RUNNING = "running"
    COMPLETED = "completed"
    ESCALATED = "escalated"
    FAILED = "failed"

class ActionResult(Enum):
    SUCCESS = "success"
    FAILURE = "failure"
    SKIPPED = "skipped"
    ESCALATE = "escalate"

@dataclass
class PlaybookContext:
    """Shared context across all playbook steps."""
    alert_id: str
    alert_type: str
    alert_data: dict
    start_time: datetime = field(default_factory=datetime.now)
    enrichment_data: dict = field(default_factory=dict)
    actions_taken: list[dict] = field(default_factory=list)
    evidence: list[dict] = field(default_factory=list)
    escalation_reason: Optional[str] = None
    status: PlaybookStatus = PlaybookStatus.RUNNING

    def add_action(self, action: str, result: ActionResult, details: dict = None) -> None:
        self.actions_taken.append({
            'timestamp': datetime.now().isoformat(),
            'action': action,
            'result': result.value,
            'details': details or {}
        })

    def set_escalation(self, reason: str) -> None:
        self.escalation_reason = reason
        self.status = PlaybookStatus.ESCALATED

class PlaybookStep(ABC):
    def __init__(self, name: str):
        self.name = name
        self.logger = logging.getLogger(f"soar.{name}")

    @abstractmethod
    def execute(self, context: PlaybookContext) -> ActionResult:
        pass

class Playbook:
    def __init__(self, name: str, steps: list[PlaybookStep]):
        self.name = name
        self.steps = steps
        self.logger = logging.getLogger(f"soar.playbook.{name}")

    def run(self, alert_id: str, alert_type: str, alert_data: dict) -> PlaybookContext:
        context = PlaybookContext(alert_id=alert_id, alert_type=alert_type, alert_data=alert_data)

        for step in self.steps:
            if context.status in [PlaybookStatus.ESCALATED, PlaybookStatus.FAILED]:
                break
            try:
                result = step.execute(context)
                context.add_action(step.name, result)
                if result == ActionResult.ESCALATE:
                    context.status = PlaybookStatus.ESCALATED
                    break
            except Exception as e:
                self.logger.error(f"Step '{step}' failed: {e}")
                context.add_action(step.name, ActionResult.FAILURE, {'error': str(e)})
                context.status = PlaybookStatus.FAILED
                break

        if context.status == PlaybookStatus.RUNNING:
            context.status = PlaybookStatus.COMPLETED

        return context

Phishing Playbook: From Triage to Containment

Phishing is the most common scenario in SOCs. A mature playbook reduces response time from 45 minutes (manual) to less than 2 minutes (automated), analyzing URLs, attachments, verifying email headers, and isolating users who clicked.

# Complete Phishing Playbook
import httpx, re, base64
from email.parser import Parser
from email.policy import default as default_policy

class ExtractEmailArtifactsStep(PlaybookStep):
    def __init__(self):
        super().__init__("extract_email_artifacts")

    def execute(self, context: PlaybookContext) -> ActionResult:
        email_raw = context.alert_data.get('email_raw', '')
        if not email_raw:
            return ActionResult.SKIPPED

        msg = Parser(policy=default_policy).parsestr(email_raw)

        headers = {
            'from': msg.get('From', ''),
            'reply_to': msg.get('Reply-To', ''),
            'dkim': 'DKIM-Signature' in msg,
            'spf': 'Received-SPF' in email_raw,
        }

        body = msg.get_body(preferencelist=('plain', 'html'))
        body_text = body.get_content() if body else ''
        urls = re.findall(r'https?://[^\s<>"]+', body_text)

        context.enrichment_data['email_headers'] = headers
        context.enrichment_data['urls'] = list(set(urls))
        context.add_evidence('extracted_urls', urls, 'email_parser')
        return ActionResult.SUCCESS

class TriageDecisionStep(PlaybookStep):
    def __init__(self):
        super().__init__("triage_decision")

    def execute(self, context: PlaybookContext) -> ActionResult:
        vt_verdict = context.enrichment_data.get('vt_verdict', 'unknown')
        headers = context.enrichment_data.get('email_headers', {})
        risk_score = 0

        if vt_verdict == 'malicious':
            risk_score += 40
        if not headers.get('dkim', True):
            risk_score += 15
        if not headers.get('spf', True):
            risk_score += 15

        context.enrichment_data['risk_score'] = risk_score

        if risk_score >= 50:
            context.enrichment_data['triage_result'] = 'high_risk'
            return ActionResult.SUCCESS
        elif risk_score >= 25:
            context.enrichment_data['triage_result'] = 'medium_risk'
            context.set_escalation(f"Risk score {risk_score}: requires human review")
            return ActionResult.ESCALATE
        else:
            context.enrichment_data['triage_result'] = 'low_risk'
            return ActionResult.SUCCESS

class ContainPhishingStep(PlaybookStep):
    def __init__(self, exchange_client, ad_client):
        super().__init__("contain_phishing")
        self.exchange = exchange_client
        self.ad = ad_client

    def execute(self, context: PlaybookContext) -> ActionResult:
        if context.enrichment_data.get('triage_result') != 'high_risk':
            return ActionResult.SKIPPED

        sender = context.alert_data.get('sender_email', '')
        if sender:
            self.exchange.add_to_blocklist(sender)

        if context.alert_data.get('user_clicked_link', False):
            victim = context.alert_data.get('recipient_email', '')
            self.ad.force_password_reset(victim)
            self.ad.revoke_sessions(victim)

        return ActionResult.SUCCESS

Malware Endpoint Playbook: Enrichment and Isolation

The malware endpoint playbook must respond more aggressively: endpoint isolation is critical to prevent lateral movement. But before isolation, it is necessary to collect as much forensic data as possible.

# Malware Endpoint Playbook
class CollectForensicDataStep(PlaybookStep):
    def __init__(self, edr_client):
        super().__init__("collect_forensic_data")
        self.edr = edr_client

    def execute(self, context: PlaybookContext) -> ActionResult:
        endpoint = context.alert_data.get('endpoint_hostname', '')
        forensics = {
            'process_tree': self.edr.get_process_tree(endpoint, context.alert_data.get('process_name')),
            'network_connections': self.edr.get_process_network(endpoint, context.alert_data.get('pid')),
            'file_changes': self.edr.get_recent_file_changes(endpoint, hours_back=2),
            'autoruns': self.edr.get_autoruns(endpoint)
        }
        context.enrichment_data['forensics'] = forensics
        context.add_evidence('forensic_collection', forensics, 'edr')
        return ActionResult.SUCCESS

class IsolateEndpointStep(PlaybookStep):
    def __init__(self, edr_client, cmdb_client):
        super().__init__("isolate_endpoint")
        self.edr = edr_client
        self.cmdb = cmdb_client

    def execute(self, context: PlaybookContext) -> ActionResult:
        verdict = context.enrichment_data.get('malware_verdict', 'unknown')
        if verdict not in ['confirmed_malware', 'suspicious']:
            return ActionResult.SKIPPED

        endpoint = context.alert_data.get('endpoint_hostname', '')
        asset_info = self.cmdb.get_asset_info(endpoint)

        if asset_info.get('criticality') == 'critical':
            context.set_escalation(
                f"Endpoint '{endpoint}' is critical (type: {asset_info.get('type')}). "
                f"Isolation requires manual approval."
            )
            return ActionResult.ESCALATE

        isolation_result = self.edr.isolate_endpoint(
            endpoint,
            reason=f"Malware detection - alert {context.alert_id}",
            allow_edr_communication=True
        )

        context.add_evidence('isolation', {
            'endpoint': endpoint,
            'result': isolation_result,
            'timestamp': datetime.now().isoformat()
        }, 'edr')

        return ActionResult.SUCCESS

Automatic Documentation and Ticket Creation

A mature playbook automatically generates incident documentation, populating the ticket with all enrichment data, actions taken, and remediation recommendations. This eliminates the "reporting overhead" that often occupies 40% of analyst time.

# Automatic Documentation with Jinja2
import jinja2

INCIDENT_REPORT_TEMPLATE = """
## Incident Report - {{ context.alert_id }}
**Status**: {{ context.status.value }}
**Alert Type**: {{ context.alert_type }}
**Risk Score**: {{ context.enrichment_data.get('risk_score', 'N/A') }}

### Actions Taken
{% for action in context.actions_taken %}
- {{ action.timestamp }} - {{ action.action }}: {{ action.result }}
{% endfor %}

### Evidence Collected
{% for ev in context.evidence %}
- {{ ev.type }} (from {{ ev.source }})
{% endfor %}

{% if context.escalation_reason %}
### Escalation Required
**Reason**: {{ context.escalation_reason }}
{% endif %}
"""

class DocumentIncidentStep(PlaybookStep):
    def __init__(self, thehive_client=None):
        super().__init__("document_incident")
        self.thehive = thehive_client
        self.template = jinja2.Template(INCIDENT_REPORT_TEMPLATE)

    def execute(self, context: PlaybookContext) -> ActionResult:
        report = self.template.render(context=context)
        context.enrichment_data['incident_report'] = report

        if self.thehive:
            case_id = context.alert_data.get('thehive_case_id')
            if case_id:
                self.thehive.update_case(case_id, description=report)
            else:
                self.thehive.create_case({
                    'title': f"[SOAR] {context.alert_type} - {context.alert_id}",
                    'description': report,
                    'severity': 2
                })

        return ActionResult.SUCCESS

Playbook Testing Framework

SOAR playbooks must be tested with the same rigor as application code. A faulty playbook in production can cause over-containment (isolating legitimate hosts) or under-containment (failing to act on real threats).

# Testing Framework for SOAR Playbooks
import pytest
from unittest.mock import MagicMock, patch

class MockExchangeClient:
    def __init__(self):
        self.blocked_senders = []

    def add_to_blocklist(self, sender: str) -> None:
        self.blocked_senders.append(sender)

    def search_and_delete(self, sender: str, url_contains: str) -> dict:
        return {"deleted_count": 3, "status": "success"}

class MockADClient:
    def __init__(self):
        self.password_resets = []

    def force_password_reset(self, user: str) -> None:
        self.password_resets.append(user)

    def revoke_sessions(self, user: str) -> None:
        pass

class TestPhishingPlaybook:
    def setup_method(self):
        self.exchange = MockExchangeClient()
        self.ad = MockADClient()

    def test_high_risk_triggers_containment(self):
        """High risk phishing must trigger containment."""
        alert = {
            "alert_id": "test-001",
            "alert_type": "phishing",
            "sender_email": "attacker@evil.com",
            "recipient_email": "victim@company.com",
            "user_clicked_link": True,
            "email_raw": "From: attacker@evil.com\n\nClick: http://evil.com/phish"
        }

        playbook = Playbook("phishing_response", [
            ExtractEmailArtifactsStep(),
            TriageDecisionStep(),
            ContainPhishingStep(self.exchange, self.ad),
        ])

        context = playbook.run("test-001", "phishing", alert)
        assert context.status in [PlaybookStatus.COMPLETED, PlaybookStatus.ESCALATED]

    def test_low_risk_no_containment(self):
        """Low risk email must NOT trigger containment."""
        alert = {
            "alert_id": "test-002",
            "alert_type": "phishing",
            "sender_email": "news@legit.com",
            "user_clicked_link": False,
            "email_raw": "From: news@legit.com\nSubject: Newsletter\n\nHello!"
        }

        playbook = Playbook("phishing_response", [
            ExtractEmailArtifactsStep(),
            TriageDecisionStep(),
            ContainPhishingStep(self.exchange, self.ad),
        ])

        context = playbook.run("test-002", "phishing", alert)
        assert len(self.exchange.blocked_senders) == 0
        assert len(self.ad.password_resets) == 0

Best Practice: Dry Run Mode

Before deploying to production, run every playbook in "dry run" mode for 2 weeks. Dry run executes all enrichment and decision steps but skips actual containment actions (isolation, blocks, password resets), only logging what it would have done. This allows calibrating triage logic without risk.

Critical: Always Check Asset Criticality Before Containment

Never automate isolation of assets without checking the CMDB first. Automatically isolating a production database or domain controller to respond to a false positive can cause a self-inflicted outage far more damaging than the original threat. Always require human approval for critical assets.

Conclusions and Key Takeaways

Python SOAR playbooks represent the frontier of modern incident response: they combine the flexibility of a general-purpose language with the richest security integration ecosystem available. The modular framework ensures reusability, testability, and maintainability over time.

Key Takeaways

  • The modular architecture (Step + Playbook + Context) is the foundation of any enterprise SOAR
  • Multi-source enrichment (VT, EDR, CMDB, AD) is essential for accurate triage decisions
  • Automatic escalation for critical assets prevents collateral damage from over-containment
  • Automatic documentation eliminates reporting overhead (40% of analyst time)
  • Testing with mock clients ensures quality without impacting production systems
  • Dry run mode is essential for calibrating triage logic before go-live

Related Articles

  • Alert Triage Automation: Reducing MTTD with Graph Analysis
  • Threat Intelligence Ingestion: STIX/TAXII Feed Processor
  • Detection Engineering as a Discipline: From Scripts to Pipeline

Frequently asked questions

What is a SOAR playbook and how does it help in incident response?

A SOAR (Security Orchestration, Automation and Response) playbook is a formal, repeatable orchestration of actions in response to a security incident that automates the process of responding to threats.

How do Python-based implementations of SOAR playbooks compare to manual incident response processes?

Recent industry data confirms that Python-based implementations have achieved 2.5 minute Mean Time to Respond (MTTR) with 92% accuracy, compared to hours for manual processes.

What is the importance of testing a SOAR playbook before deploying it in production?

A faulty playbook can cause over-containment or under-containment, and therefore must be tested with the same rigor as application code using a "dry run" mode that simulates containment actions without actually taking them.

Why should human approval always be required for critical assets such as production databases or domain controllers?

Automatically isolating these assets can cause a self-inflicted outage far more damaging than the original threat, and therefore requires manual intervention to prevent unintended consequences.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects