elastic / elastic/integrations
[okta] Capture new securityContext.ipDetails fields from Okta 2026.04.0
- Dominant language
- Handlebars
- Stars
- 333
- Forks
- 647
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 209
Description
_Drafted by Claude._
## Summary
Okta's [Production 2026.04.0 release](https://help.okta.com/en-us/content/topics/releasenotes/production.htm) adds new structured fields under `securityContext.ipDetails.ipServiceCategories[]` in System Log events. These fields describe whether the source IP is an anonymizing service (VPN, Proxy, Tor, etc.) and identify the operator. The Okta integration currently does not capture them, and because the ingest pipeline removes the `json` scratch object at the end of processing, the new fields are silently dropped before indexing.
We should update the `okta.system` data stream to extract and map these fields.
## Background
From the Okta release notes:
> **New System Log objects for security.request.blocked events**
>
> The System Log now displays the following IpDetails objects for dynamic and enhanced dynamic zones:
>
> - **Operator** indicates whether the type is VPN or Proxy
> - **Type** includes values like VPN, Proxy, and Tor
> - **IsAnonymous** indicates if the proxy is anonymous
>
> These objects move risk and behavior telemetry out of string-only keys in the debug context and into dedicated, structured fields in the security context event. This change improves risk visibility and eliminates the need for string parsing.
Per the [Okta System Log API reference](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/systemlog/other/listlogevents#other/listlogevents/t=response&c=200&path=securitycontext/ipdetails/ipservicecategories), the shape is:
```
securityContext.ipDetails.ipServiceCategories[]
.isAnonymous boolean | null — whether the service is an anonymizer
.operator string | null — the name of the associated operator
.type string | null — VPN | Proxy | Tor | Residential Proxy | Blockchain VPN
```
`ipServiceCategories` is an array because a single IP may be classified under multiple service types by Okta's upstream feeds (e.g., both `Residential Proxy` and `Proxy`, or a Tor exit that also runs on a commercial VPN).
## Current behavior
`packages/okta/data_stream/system/elasticsearch/ingest_pipeline/default.yml` parses the raw log into a scratch `json` field, renames known `securityContext.*` keys (`asNumber`, `asOrg`, `domain`, `isProxy`, `isp`) into `okta.security_context.*`, and then [removes the entire `json` field](https://github.com/elastic/integrations/blob/main/packages/okta/data_stream/system/elasticsearch/ingest_pipeline/default.yml) near the end of the pipeline. There are no references to `ipDetails`, `ipServiceCategories`, `isAnonymous`, or related names anywhere in `packages/okta/`. As a result:
- No mapping errors will occur when the new fields arrive (so this is not a breakage).
- But the telemetry is lost — exactly the telemetry Okta says should eliminate string parsing in detection rules.
## Existing string parsing in `debugContext`
The release notes describe `ipDetails` as moving telemetry "out of string-only keys in the debug context." It's worth being concrete about which keys: the current `okta.system` ingest pipeline already performs brittle regex/`kv` parsing over several `debugContext.debugData` fields, which is exactly the pattern the new structured fields are meant to retire.
- **`debugContext.debugData.behaviors`** — raw string like `"{New Geo-Location=NEGATIVE, New Device=POSITIVE, ...}"`. The pipeline strips the outer braces with `dissect`, runs a `kv` processor to turn it into a map under `okta.debug_context.debug_data.behaviors`, and then a Painless script promotes any `POSITIVE` keys into `okta.debug_context.debug_data.risk_behaviors`. See `no_use_flattened_debug.yml:11-29, 108-123` and the equivalent block in `use_flattened_debug.yml`.
- **`debugContext.debugData.risk`** — raw string like `"{reasons=Anomalous Device, Anomalous Location, level=HIGH}"`. Parsed with `dissect` + `kv`, with `grok` fallbacks for `level` and `reasons`. Materialized as `okta.debug_context.debug_data.risk_level` and `okta.debug_context.debug_data.risk_reasons`. See `no_use_flattened_debug.yml:30-74`.
- **`debugContext.debugData.logOnlySecurityData`** — JSON-encoded string. Parsed via the `json` processor; `risk.level` / `risk.reasons` are hoisted into the same `risk_level` / `risk_reasons` fields as a preferred source. See `no_use_flattened_debug.yml:8-10, 76-83`.
- **`debugContext.debugData.tunnels`** — JSON-encoded string. Parsed in place via the `json` processor but no subfields are extracted; the parsed object sits under `okta.debug_context.debug_data.tunnels`. The name strongly suggests this is the closest existing precursor to `ipServiceCategories` (VPN/Tor/proxy tunnel data), and is the most likely "string-only key" the release notes are referring to — though confirmation from Okta would be welcome.
The existing extraction is not just legacy cruft — `risk_behaviors`, `risk_level`, and `risk_reasons` carry signals (geolocation anomalies, device anomalies, velocity) that `ipDetails` does not. The new fields are **additive** IP-reputation telemetry, not a wholesale replacement, so this proposal does not touch the existing debug-context parsing.
## ECS research
I consulted ECS for a native home for these fields. **None exists**:
- `source.as.organization.name` / `client.as.organization.name` — locked to ASN owner, not VPN operator brand, and already populated from `securityContext.asOrg`.
- `network.type` — OSI layer-3 family, not anonymization service type.
- `threat.indicator.*` — designed for IOC records ingested from threat feeds, not inline IP-reputation annotation on authentication events.
- `organization.name` — refers to the monitored org, not third-party operators.
There is no ECS boolean for "IP is an anonymizer," no enum for VPN/Tor/Proxy, and no field for anonymization-service operator names. If anyone is aware of an in-flight ECS RFC in this space, please link it here — we'd rather align than mint custom fields that get deprecated.
## Proposal
Add three new custom fields under `okta.security_context.ip_details` and populate them in `default.yml`, **without modifying the existing `okta.debug_context.debug_data.*` extraction**. The new fields are additive: they should coexist with `risk_behaviors`, `risk_level`, `risk_reasons`, and `tunnels` so that historical data and non-IP-reputation signals (geolocation, device, velocity anomalies) keep working.
The Okta integration has **zero** `type: nested` fields today (verified via grep), and ES|QL does not support querying nested fields, so the array is flattened into parallel `keyword` multi-values on ingest. Per-item correlation between a specific `type` and its `operator` is lost by flattening, but detection rules generally ask "is this Tor/VPN?" and "who's the operator?" independently, so the trade is worth it.
### Fields
In `packages/okta/data_stream/system/fields/fields.yml`, under the existing `okta.security_context` group:
```yaml
- name: ip_details
type: group
fields:
- name: is_anonymous
type: boolean
description: >-
True if any entry in Okta's ipServiceCategories reports the source
IP as an anonymizing service.
- name: service_type
type: keyword
description: >-
Service classifications reported by Okta for the source IP
(e.g., VPN, Proxy, Tor, Residential Proxy, Blockchain VPN).
- name: service_operator
type: keyword
description: >-
Operator names associated with the source IP's service
classifications (e.g., the VPN provider).
```
### Pipeline
In `packages/okta/data_stream/system/elasticsearch/ingest_pipeline/default.yml`, alongside the existing `json.securityContext.*` rename block, add a processor (script or foreach) that iterates `json.securityContext.ipDetails.ipServiceCategories[]` and populates the three flattened fields, deduped, before the `remove: field: json` step. `isAnonymous` should be ORed across entries.
### Tests
Add pipeline test cases covering:
- An IP with a single categorization (e.g., Tor).
- An IP with multiple categorizations (e.g., `Residential Proxy` + `Proxy`).
- An IP where some array entries have null `operator` or null `type`.
- An event without `ipDetails` (regression check).
### Changelog
One `enhancement` / `minor` entry referencing this issue and the Okta 2026.04.0 release.
## Out of scope
- Detection-rule updates that consume the new fields (separate repo).
- Migration of any existing string-parsed debug-context risk data
## References
- Okta Production release notes: https://help.okta.com/en-us/content/topics/releasenotes/production.htm
- Okta System Log API reference: https://developer.okta.com/docs/api/openapi/okta-management/management/tag/SystemLog/
Contributor guide
Assessment
This issue has not been assessed yet.