Enterprise Security Adaptive Response
- How It Fits Together
- Registering the Action
- The Action Itself
- Writing Back to the Notable
- Risk-based Alerting
- Ad-hoc Actions from Incident Review
- Testing Before You Register It
Adaptive response actions in Enterprise Security run from a notable event, either automatically or when an analyst picks them from the incident review menu. Splunk ships a handful. Writing your own normally means building an add-on.
With BabySOARus, an adaptive response action is a saved search containing exec.
How It Fits Together
ES runs an adaptive response action by dispatching a saved search with the
notable's fields available as tokens. If that saved search contains exec,
your Python runs with the notable's context and can do whatever the action
needs: enrich, decide, call an API, write back to a lookup.
correlation search -> notable event -> adaptive response -> saved search
|
| exec
|
enrich, decide, act
Registering the Action
default/savedsearches.conf in your ES content app:
[Enrich and triage notable]
action.notable = 0
dispatch.earliest_time = -5m
dispatch.latest_time = now
search = | makeresults \
| eval src = "$src$", dest = "$dest$", user = "$user$", \
rule_name = "$rule_name$", urgency = "$urgency$" \
| exec inline="..."
[action_adaptive_response]
disabled = 0
And default/alert_actions.conf to make it selectable:
[enrich_and_triage_notable]
is_custom = 1
label = Enrich and triage (BabySOARus)
description = Enrich a notable with asset, identity and intelligence context, and set a recommended action
payload_format = json
param._cam = {"category": ["Information Gathering"], \
"task": ["update"], \
"subject": ["endpoint"], \
"technology": [{"vendor": "BabySOARus", "product": "BabySOARus"}], \
"supports_adhoc": true, \
"drilldown_uri": "search?q=..."}
The param._cam block is what makes ES show the action in the incident review
menu and in the correlation search editor.
The Action Itself
| makeresults
| eval src="$src$", dest="$dest$", user="$user$", rule_name="$rule_name$"
| exec inline="
findings = []
for e in events:
src, dest, user = e.get('src', ''), e.get('dest', ''), e.get('user', '')
# 1. Asset context, from ES's own asset framework.
asset = {}
r = splunk_request('GET',
f'/servicesNS/nobody/SA-IdentityManagement/storage/collections/data/asset_lookup?query={{\"ip\":\"{dest}\"}}&output_mode=json')
if r['status'] == 200:
rows = json.loads(r['body'])
asset = rows[0] if rows else {}
e['asset_owner'] = asset.get('owner', 'unassigned')
e['asset_priority'] = asset.get('priority', 'unknown')
e['asset_category'] = asset.get('category', '')
# 2. Have we seen this pair before?
r = splunk_request('GET',
f'/services/search/jobs/export?output_mode=json&search='
f'search index=notable src={src} dest={dest} earliest=-30d | stats count')
seen_before = 0
if r['status'] == 200:
for line in r['body'].decode().splitlines():
if line.strip():
try:
seen_before = int(json.loads(line)['result']['count'])
except (KeyError, ValueError):
pass
e['prior_notables_30d'] = seen_before
# 3. Decide.
if e['asset_priority'] in ('critical', 'high') and seen_before == 0:
e['recommendation'] = 'escalate to tier 2 immediately'
e['new_urgency'] = 'critical'
elif seen_before > 5:
e['recommendation'] = 'likely known-good pattern, review suppression'
e['new_urgency'] = 'low'
else:
e['recommendation'] = 'standard triage'
e['new_urgency'] = 'medium'
findings.append(e)
events = findings"
| table src dest user asset_owner asset_priority prior_notables_30d recommendation new_urgency
Three enrichments and a decision, in one place, readable end to end.
Writing Back to the Notable
Adaptive response actions commonly update the notable's status or urgency. The ES REST endpoint takes the notable's event ID:
r = splunk_request(
'POST',
'/services/notable_update',
body={
'ruleUIDs': [e['event_id']],
'status': '2', # in progress
'urgency': e['new_urgency'],
'comment': f"BabySOARus triage: {e['recommendation']}",
},
)
e['notable_updated'] = r['status'] == 200
Pass $event_id$ into the search alongside the other tokens to get it.
Risk-based Alerting
If you use RBA, the same mechanism assigns risk scores with logic rather than a static number per rule.
| exec inline="
for e in events:
score = 20
if e.get('asset_priority') == 'critical':
score += 40
elif e.get('asset_priority') == 'high':
score += 25
if e.get('user', '').startswith('svc_'):
score += 15 # service accounts should not do this
if int(e.get('prior_notables_30d', 0)) == 0:
score += 20 # never seen before
hour = int(e.get('_time_hour', 12))
if hour < 6 or hour > 20:
score += 10 # out of hours
e['risk_score'] = min(100, score)
e['risk_object'] = e.get('dest', '')
e['risk_object_type'] = 'system'"
| collect index=risk
The contributing factors are visible in the code, which is the thing that makes an RBA implementation maintainable a year later.
Ad-hoc Actions from Incident Review
Setting "supports_adhoc": true in param._cam lets an analyst run the
action manually from a notable. That is where BabySOARus is most useful: a menu
of small, purposeful actions, each a few lines of Python.
Actions worth having:
| Action | What it does |
|---|---|
| Enrich indicators | Look up every IP, domain and hash on the notable in one API call |
| Check asset ownership | Resolve owner, business unit and criticality |
| Historical context | Count similar notables in the last 30 days |
| Decode payload | Base64 and URL decode anything embedded in the notable |
| Draft ticket | Build a formatted summary ready to paste, or post it directly |
Each is a saved search with an exec snippet, and each takes a few minutes to
write.
Testing Before You Register It
An adaptive response action is just a search, so test it as one. Substitute real values for the tokens and run it interactively:
| makeresults
| eval src="10.20.0.17", dest="10.20.0.51", user="d.harrington", rule_name="Test"
| exec inline="..."
That is the whole development loop. No add-on to reinstall, no ES restart.