Playbook (SOAR)
Playbook is the SOAR (Security Orchestration, Automation and Response) engine of CSIRT-Pro. Build visual automation flows with a drag-and-drop editor to automate detection, triage, notification, and response workflows.
Response actions are approval-gated
A playbook can enrich, evaluate, and notify on its own. It does not block traffic, remediate, or close a case automatically. Any change a playbook makes to an external system is something you script explicitly, and SOAR write actions in CSIRT-Pro follow a request, approve, and apply flow.
Screen Layout
The Playbook editor has the following components.
- Flow editor, a visual canvas for placing and connecting nodes.
- Code editor for writing Python or JavaScript in each node.
- Test panel for running the playbook with sample data.
- Execution history for past execution results and logs.
- Version control for browsing and restoring playbook versions.
- Environment variables for secrets and configuration values.
Core Concepts
Node Types
| Type | Type ID | Description |
|---|---|---|
| Action | 1 |
Execute logic such as HTTP requests, data transformations, API calls, and notifications |
| Condition (If/Else) | 2 |
Branch the flow on a true or false evaluation |
| Loop | 4 |
Iterate over a collection of items |
Trigger Types
A playbook can be triggered automatically by the following events.
| Trigger | Type ID | Description |
|---|---|---|
| Manual | -- | Run from the UI or through the API |
| Webhook | -- | Triggered by an external HTTP POST request |
| Schedule | 104 |
Cron-based periodic execution |
| Case Creation | -- | Runs automatically when a new case is created |
Creating a Playbook
1. Create a New Playbook
From the Playbook list screen, click Create New and assign a name.
2. Add Nodes
- Right-click the flow editor canvas and select Add Node.
- Choose a node type: Action, Condition, or Loop.
- Assign a name to the node.
3. Write Code
Click a node to open the code editor. Write Python or JavaScript to implement the node's logic.
Key variables available in every node:
| Variable | Description |
|---|---|
input_data |
Data passed from the previous node, or the trigger payload |
env |
Dictionary of environment variables |
output |
Assign the node's output to this variable |
4. Connect Nodes
Drag connections between nodes to define the execution flow.
5. Configure a Trigger
Set the trigger for the playbook.
# Schedule examples (cron format)
*/5 * * * * # Every 5 minutes
0 * * * * # Every hour
0 0 * * * # Every day at midnight
0 9 * * 1 # Every Monday at 9:00 AM
Node Type Details
Action Node (type = 1)
Runs arbitrary code.
The result is stored in output and passed to the next node as input_data.
{
"type": 1,
"name": "send_slack_alert",
"code": "import requests\n\nresponse = requests.post(\n 'https://slack.com/api/chat.postMessage',\n headers={'Authorization': f'Bearer {env[\"SLACK_TOKEN\"]}'},\n json={'channel': '#alerts', 'text': f'Alert: {input_data[\"message\"]}'}\n)\n\noutput = response.json()"
}
Condition Node (type = 2)
Evaluates a boolean expression.
The flow branches to the true or false path based on the result.
{
"type": 2,
"name": "check_severity",
"code": "output = input_data['severity'] == 'high'",
"true": [
{"type": 1, "name": "escalate", "code": "..."}
],
"false": [
{"type": 1, "name": "log_only", "code": "..."}
]
}
Loop Node (type = 4)
Iterates over an array. Each item is processed in turn by the child nodes.
Code Examples
Python: HTTP Request
import requests
response = requests.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {env['SLACK_TOKEN']}"},
json={
"channel": "#security-alerts",
"text": f"New alert: {input_data['message']}"
}
)
output = response.json()
Python: Query an External API
import requests
ip = input_data['src_ip']
response = requests.get(
f"https://api.abuseipdb.com/api/v2/check",
headers={
"Key": env['ABUSEIPDB_KEY'],
"Accept": "application/json"
},
params={"ipAddress": ip, "maxAgeInDays": 90}
)
result = response.json()
output = {
"ip": ip,
"abuse_score": result['data']['abuseConfidenceScore'],
"is_malicious": result['data']['abuseConfidenceScore'] > 80
}
Python: Create a Case via API
import requests
response = requests.post(
f"https://{env['CSIRT_DOMAIN']}/api/user/{env['USER_ID']}/messages/",
headers={"Cookie": f"jwt={env['ACCESS_TOKEN']}"},
json={
"title": f"Suspicious activity from {input_data['src_ip']}",
"description": f"AbuseIPDB score: {input_data['abuse_score']}",
"status": "open",
"tags": {"severity": "high", "source": "playbook"}
}
)
output = response.json()
JavaScript: Webhook Notification
const response = await fetch('https://hooks.example.com/webhook', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
event: 'security_alert',
message: input_data.message,
severity: input_data.severity
})
});
output = await response.json();
JavaScript: Data Transformation
const logs = input_data.logs;
const summary = logs.reduce((acc, log) => {
const ip = log.src_ip;
if (!acc[ip]) acc[ip] = 0;
acc[ip]++;
return acc;
}, {});
const topIPs = Object.entries(summary)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ip, count]) => ({ ip, count }));
output = { top_ips: topIPs };
Environment Variables
Store secrets and configuration values securely as environment variables.
Setting Environment Variables
- Open the Environment Variables tab in the Playbook editor.
- Enter a key and value, then save.
- Access it in code with
env['KEY_NAME'].
Security
Environment variables are encrypted at rest on the server side. Values are masked in the UI.
Common Environment Variables
| Key | Example Value | Usage |
|---|---|---|
SLACK_TOKEN |
xoxb-xxx-xxx |
Slack Bot token for notifications |
ABUSEIPDB_KEY |
abc123... |
AbuseIPDB API key for IP reputation checks |
CSIRT_DOMAIN |
app.csirt-pro.example.com |
CSIRT-Pro domain for internal API calls |
USER_ID |
user-uuid |
Service account user ID |
ACCESS_TOKEN |
eyJhbGciOi... |
Service account access token |
Test Execution
Test a playbook with sample data before deploying it to production.
Test Procedure
- Open the Test tab.
- Enter sample input data as JSON.
- Click Run Test.
- Review the output of each node.
Autotest
Automated test suites can be configured for a playbook.
GET /api/user/<user_id>/playbook/<playbook_id>/autotest/gets the test config.PUT /api/user/<user_id>/playbook/<playbook_id>/autotest/updates the test config.POST /api/user/<user_id>/playbook/<playbook_id>/autotest/executeruns the autotest.
Execution History
Playbook execution results are recorded as history entries.
| Field | Description |
|---|---|
| Execution ID | Unique identifier for each execution |
| Status | pending / running / success / failed |
| Start Time | Execution start timestamp |
| End Time | Execution end timestamp |
| Logs | Per-node execution logs and outputs |
| Error Message | Error details for a failed execution |
Retry Failed Executions
A failed execution can be retried by specifying its execution ID.
Version Control
Every change to a playbook is recorded as a version.
- Browse past versions.
- Restore a specific version.
- Compare versions side by side.
Playbook Merge
Two playbooks can be merged into one. Nodes from the source playbook are added to the target playbook.
Webhook Triggers
An external system can trigger a playbook through its webhook endpoint.
curl -X POST "https://<domain>/api/user/<user_id>/playbook/<playbook_id>/webhook/" \
-H "Content-Type: application/json" \
-H "Cookie: jwt=<token>" \
-d '{
"alert_type": "brute_force",
"src_ip": "10.0.0.50",
"count": 150
}'
The JSON body is available in the playbook as input_data.
Detailed Flow Examples
Example 1: Alert Triage Flow
Triage an incoming alert automatically: check IP reputation, evaluate severity, create a case for a high-severity alert, and notify the team.
[Webhook Trigger]
│
▼
[Action: Check IP Reputation]
│
▼
[Condition: Severity High?]
│ │
True False
│ │
▼ ▼
[Action: [Action:
Create Log to
Case] Pipeline]
│
▼
[Action: Notify Slack]
Node 1, Check IP Reputation (Action, type=1):
import requests
ip = input_data['src_ip']
response = requests.get(
"https://api.abuseipdb.com/api/v2/check",
headers={"Key": env['ABUSEIPDB_KEY'], "Accept": "application/json"},
params={"ipAddress": ip, "maxAgeInDays": 90}
)
data = response.json()['data']
output = {
"src_ip": ip,
"abuse_score": data['abuseConfidenceScore'],
"country": data.get('countryCode', 'Unknown'),
"message": input_data.get('message', ''),
"severity": "high" if data['abuseConfidenceScore'] > 80 else "low"
}
Node 2, Severity Check (Condition, type=2):
Node 3, Create Case (Action, type=1), true branch:
import requests
response = requests.post(
f"https://{env['CSIRT_DOMAIN']}/api/user/{env['USER_ID']}/messages/",
headers={"Cookie": f"jwt={env['ACCESS_TOKEN']}"},
json={
"title": f"High-risk IP detected: {input_data['src_ip']}",
"description": f"AbuseIPDB score: {input_data['abuse_score']}, Country: {input_data['country']}",
"status": "open",
"tags": {"severity": "high", "source": "auto-triage"}
}
)
output = {"case_id": response.json().get("id"), **input_data}
Node 4, Notify Slack (Action, type=1):
import requests
requests.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {env['SLACK_TOKEN']}"},
json={
"channel": "#security-alerts",
"text": (
f"*High-Severity Alert*\n"
f"IP: `{input_data['src_ip']}` (Score: {input_data['abuse_score']})\n"
f"Country: {input_data['country']}\n"
f"Case ID: {input_data.get('case_id', 'N/A')}"
)
}
)
output = {"status": "notified"}
Example 2: IP Blocking Flow
When a case is created with a malicious IP, block the IP on the firewall and notify the team. The firewall write here is an action you script and operate under your own controls, not something CSIRT-Pro performs on its own.
[Case Creation Trigger]
│
▼
[Action: Extract IOCs]
│
▼
[Loop: For Each IP]
│
▼
[Condition: Score > 90?]
│ │
True False
│ │
▼ ▼
[Action: [Action:
Block on Skip]
Firewall]
│
▼
[Action: Log Action]
Node 1, Extract IOCs (Action, type=1):
import re
text = input_data.get('description', '') + ' ' + input_data.get('title', '')
ip_pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
ips = list(set(re.findall(ip_pattern, text)))
output = {"ip_list": ips, "case_id": input_data.get("id")}
Node 2, Loop Over IPs (Loop, type=4):
Node 3, Check Score (Condition, type=2):
import requests
ip = input_data # current loop item
response = requests.get(
"https://api.abuseipdb.com/api/v2/check",
headers={"Key": env['ABUSEIPDB_KEY'], "Accept": "application/json"},
params={"ipAddress": ip, "maxAgeInDays": 90}
)
score = response.json()['data']['abuseConfidenceScore']
output = score > 90
Node 4, Block on Firewall (Action, type=1), true branch:
import requests
response = requests.post(
f"https://{env['FIREWALL_API']}/api/v1/block",
headers={"Authorization": f"Bearer {env['FIREWALL_TOKEN']}"},
json={"ip": input_data, "duration": "24h", "reason": "Auto-blocked by CSIRT-Pro"}
)
output = {"blocked_ip": input_data, "status": response.status_code}
Example 3: Scheduled Threat Hunt Flow
Run every 15 minutes to search recent logs for known malicious indicators and create a case for any match.
Trigger: Schedule (type=104), cron */15 * * * *.
[Schedule: Every 15 min]
│
▼
[Action: Query Recent Logs]
│
▼
[Condition: Matches Found?]
│ │
True False
│ │
▼ ▼
[Loop: [Action:
Each Log No
Match] Findings]
│
▼
[Action: Enrich with TI]
│
▼
[Action: Create Case]
Node 1, Query Recent Logs (Action, type=1):
import requests
response = requests.post(
f"https://{env['CSIRT_DOMAIN']}/api/user/{env['USER_ID']}/search/",
headers={"Cookie": f"jwt={env['ACCESS_TOKEN']}"},
json={
"expr": "from `firewall_logs` filter action == \"DENY\" filter __time > @2025-01-15T10:00 sort {-__time} take 500"
}
)
# The search API returns results in a columnar binary format; read them with pyarrow.
import pyarrow as pa
reader = pa.ipc.open_stream(response.content)
table = reader.read_all()
df = table.to_pandas()
suspicious = df[df['src_ip'].isin(env.get('WATCHLIST_IPS', '').split(','))]
output = {
"matches": suspicious.to_dict('records'),
"match_count": len(suspicious)
}
Node 2, Check Matches (Condition, type=2):
Node 3, Loop Each Match (Loop, type=4):
Node 4, Enrich with Threat Intelligence (Action, type=1):
import requests
ip = input_data['src_ip']
response = requests.get(
"https://api.abuseipdb.com/api/v2/check",
headers={"Key": env['ABUSEIPDB_KEY'], "Accept": "application/json"},
params={"ipAddress": ip, "maxAgeInDays": 90}
)
data = response.json()['data']
output = {
**input_data,
"abuse_score": data['abuseConfidenceScore'],
"country": data.get('countryCode', 'Unknown'),
"isp": data.get('isp', 'Unknown')
}
Node 5, Create Case (Action, type=1):
import requests
match = input_data
response = requests.post(
f"https://{env['CSIRT_DOMAIN']}/api/user/{env['USER_ID']}/messages/",
headers={"Cookie": f"jwt={env['ACCESS_TOKEN']}"},
json={
"title": f"Threat hunt match: {match['src_ip']}",
"description": (
f"Source IP: {match['src_ip']}\n"
f"AbuseIPDB Score: {match['abuse_score']}\n"
f"Country: {match['country']}\n"
f"ISP: {match['isp']}\n"
f"Action: {match.get('action', 'N/A')}"
),
"status": "open",
"tags": {"severity": "high", "source": "threat-hunt"}
}
)
output = {"case_created": True, "case_id": response.json().get("id")}
API Operations
Playbooks can be managed and run through the REST API. See the API Reference for full endpoint documentation, including the following.
GET /api/user/<user_id>/playbook/lists playbooks.POST /api/user/<user_id>/playbook/creates a playbook.POST /api/user/<user_id>/playbook/<playbook_id>/execute/runs a playbook.POST /api/user/<user_id>/playbook/<playbook_id>/webhook/is the webhook trigger.GET /api/user/<user_id>/playbook/<playbook_id>/histories/returns execution history.GET /api/user/<user_id>/playbook/<playbook_id>/versions/returns version history.GET /api/user/<user_id>/playbook/<playbook_id>/environments/returns environment variables.POST /api/user/<user_id>/playbook/<playbook_id>/merge/<target_id>/merges playbooks.