Your Okta detection fires. A privilege grant, the kind you want eyes on fast. The Slack alert message lands and tells you the actor "" just handed out admin. Two empty quotes where the name should be.
The search is correct. The field is populated. Run the SPL by hand and you'll see the display name sitting right there in the results. But the alert went out with a blank, and Splunk never said a word about it – not in the job log, not in the alert action, nowhere.
This is the part worth your attention: every test you'd normally run passes. The detection is, by every check that happens inside Splunk's search pipeline, working. It's only broken in the one place you don't habitually look – the rendered message a human actually reads.
A correct search is not a working detection.
The shape of it
The detection pulls from a JSON source – Okta, in this case, but it could be CloudTrail, Google Workspace, M365, GitHub audit, anything that ships nested JSON. The field you want in the alert lives inside a JSON array. Okta puts the affected user under target, an array, so spath extracts it as a field literally named target{}.displayName.
The alert action references it the obvious way:
actor=$result.actor.alternateId$ target=$result.target{}.displayName$
The actor token resolves. The target token comes back empty. Same message, same syntax, one renders and one doesn't. The only difference is the curly braces.
Two faults, stacked
It's tempting to call this one bug. It's two, and you have to fix both.
Fault A: the token engine rejects the braces. Splunk's alert-action token substituter – the same engine behind dashboard input tokens – only accepts letters, digits, underscores, and dots inside a $...$ reference. Curly braces aren't on the list, and they're reserved for Splunk's own template constructs. When the engine scans result.target{}.displayName, it either stops at the { and goes looking for a field plainly named target (there isn't one – spath only ever created target{}.*), or it fails to match a valid token at all. Both roads end at an empty string.
Fault B: the field is multivalue. The {} in spath's output isn't decoration. It means "this came from a JSON array, it holds multiple values." A scalar token slot has no rule for rendering a multivalue field. Splunk doesn't pick the first value, or join them, or guess – it substitutes nothing.
Fault B is the one people miss. Rename the field to strip the braces and you've satisfied Fault A, but the token is still empty, because the field is still multivalue. Here's the full truth table:
| What you reference | Grammar OK? | Scalar? | Renders |
|---|---|---|---|
$result.target{}.displayName$ |
No | No | empty |
$result.target_array$ (rename only) |
Yes | No | empty |
$result.target_first$ (rename + mvindex) |
Yes | Yes | the name |
And the reason none of this ever pages you: token substitution has no "field not found" path that reaches the user. It's silent on purpose. Dashboard tokens are routinely optional, meant to disappear quietly when unset. The alert engine inherits that behavior and applies it to your detection, where a missing field is anything but optional.
The fix is one line doing three jobs
| eval target_displayName=mvindex('target{}.displayName', 0)
Drop it in before the alert action sees the data. Each piece is load-bearing:
- The single quotes tell SPL to read
target{}.displayNameas a literal field name. SPL itself has no problem with braces in field names – only the alert template engine does. Double quotes would make it a string literal, backticks would make it a macro. Single quotes are the only form that works here, and getting this wrong is the most common way people copy the pattern and stay broken. -
mvindex(..., 0)collapses the multivalue field to its first element. That kills Fault B. - The rename to
target_displayNamegives the token a name built from characters the grammar accepts. That kills Fault A.
Now $result.target_displayName$ resolves, and the analyst sees a name.
Prove it to yourself in any environment
Don't trust a search that looks right. Trust what the alert renders. You can isolate Fault A in one query by putting a known-good token next to the broken one:
index=security sourcetype="OktaIM2:log" earliest=-1h
| head 1
| eval token_test="actor=$result.actor.alternateId$ target=$result.target{}.displayName$"
| table actor.alternateId target{}.displayName token_test
The actor.alternateId half of token_test renders. The target{}.displayName half comes back empty, sitting right next to a table column that proves the underlying value exists. That gap – populated in the table, blank in the token – is the whole bug in one screen.
Then go wider. Grep your saved searches for {} inside any $...$ reference. Every match is a detection that's been shipping blanks. And bake the normalize-to-scalar step into your detection template, so multivalue JSON fields never reach an alert action with their braces intact.
The actual lesson
The search was never wrong. That's what makes this one mean. There's no syntax error to catch, no failed job to investigate, no red in CI. The field is in your results the entire time. The detection passes every test that runs inside the pipeline, and the only symptom is a name that isn't there – visible solely in Slack, and only if the analyst happens to notice the quotes are empty.
Test the render, not the search. The search tells you the detection works. Only the alert tells you it's useful.
Top comments (0)