[sentinel_one] Add Support for Optional Threat Timeline Collection#19990
[sentinel_one] Add Support for Optional Threat Timeline Collection#19990mohitjha-elastic wants to merge 3 commits into
Conversation
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
Elastic Docs Style Checker (Vale)Summary: 2 suggestions found 💡 Suggestions (2): Optional style improvements. Apply when helpful.
The Vale linter checks documentation changes against the Elastic Docs style guide. To use Vale locally or report issues, refer to Elastic style guide for Vale. |
| "cursor": (size(body.?data.orValue([])) > 0) ? | ||
| body.data.map(e, e.?threatInfo.updatedAt.orValue(null)).filter(v, v != null).as(updates, | ||
| updates.size() > 0 ? | ||
| !has(state.worklist) ? // Exit early due to GET failure. |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/sentinel_one/data_stream/threat/agent/stream/httpjson_as_cel.yml.hbs:96
A list-fetch error after the first successful page is silently swallowed: the phase-2 empty-worklist branch overwrites the phase-1 error event because the !has(state.worklist) guard never fires once worklist has been set.
Details
When the threat-list request fails (non-200), phase 1 returns a single-object error {"events": {"error": {...}}, "want_more": false} and does not set worklist. Phase 2 is meant to bail out via !has(state.worklist) ? state. However, draining a page sets worklist to {"data": tail(...)}, which becomes {"data": []} for the last item and is never removed. So after the first successful fetch in a session, has(state.worklist) is permanently true. On the next interval, if the list fetch errors, state.worklist is still the stale {"data": []}, so phase 2 skips the guard, falls into the empty-worklist branch, and state.with({"events": [], ...}) overwrites the error object produced by phase 1. The result: the ERROR log / degraded-status signal is lost and the failure is silently skipped (the cursor is untouched so data is retried next interval, but the error is never surfaced). The // Exit early due to GET failure guard is effectively dead code after the first evaluation.
Recommendation:
Preserve a phase-1 error before entering the timeline stage. Because events is cleared between evaluations and phase-1 success never sets events, has(state.events) reliably means "the list fetch errored" — short-circuit on it:
).as(state,
state.with(
has(state.events) ? // list fetch failed this evaluation; preserve the error and skip the timeline stage.
state
: !has(state.worklist) ?
state
: (size(state.?worklist.?data.orValue([])) > 0) ?
state.worklist.data[0].as(threat,
// ... unchanged timeline logic ...
)
:
(state.?next_page.token.orValue(null) != null) ?
{"events": [{"retry": true}], "want_more": true}
:
{"events": [], "want_more": false, "list_updated_at_gte": ""}
)
)
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| (state.?next_page.token.orValue(null) != null) ? | ||
| { | ||
| "events": [], | ||
| "want_more": true, |
There was a problem hiding this comment.
Severity: 🔵 Low confidence: medium path: packages/sentinel_one/data_stream/threat/agent/stream/httpjson_as_cel.yml.hbs:206
The empty-worklist branch returns want_more: true with an empty events array; the input only re-evaluates on a non-empty events, so an empty threat-list page carrying a non-null nextCursor stalls pagination until the next interval instead of following the cursor immediately.
Details
In the phase-2 empty-worklist branch, when state.next_page.token is present the program returns {"events": [], "want_more": true}. The CEL input triggers an immediate re-evaluation on want_more: true only when at least one event was published; with an empty events array the pending next_page.token is not followed within the current collection cycle. Collection does resume on the next scheduled interval (state persists next_page.token and phase 1 re-fetches with the cursor), so this is not a permanent stall, but pages after an empty-but-more list response are delayed by one interval. This path is reached only if the list API returns an empty data array together with a non-null pagination.nextCursor.
Recommendation:
Emit a placeholder event on the continuation path so the re-evaluation actually fires, and drop it before indexing:
(state.?next_page.token.orValue(null) != null) ?
{"events": [{"retry": true}], "want_more": true}
:
{"events": [], "want_more": false, "list_updated_at_gte": ""}
and discard the placeholder in the agent processors (or the ingest pipeline):
processors:
- drop_event.when.equals.retry: true🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
🚀 Benchmarks reportTo see the full report comment with |
| ( | ||
| state.?want_more.orValue(false) ? | ||
| state.list_updated_at_gte | ||
| (size(state.?worklist.?data.orValue([])) > 0) ? |
There was a problem hiding this comment.
| (size(state.?worklist.?data.orValue([])) > 0) ? | |
| state.?worklist.data[0].hasValue() ? |
| "list_updated_at_gte": (state.?cursor.?last_update_at.orValue("") != "") ? | ||
| string(state.cursor.last_update_at) | ||
| : | ||
| (now - duration(state.initial_interval)).format(time_layout.RFC3339), |
There was a problem hiding this comment.
| "list_updated_at_gte": (state.?cursor.?last_update_at.orValue("") != "") ? | |
| string(state.cursor.last_update_at) | |
| : | |
| (now - duration(state.initial_interval)).format(time_layout.RFC3339), | |
| "list_updated_at_gte": state.?cursor.last_update_at.orValue((now - duration(state.initial_interval)).format(time_layout.RFC3339)), |
This will require rejigging the code below. In general, avoid using "", null, 0 etc as a sentinel for absence; we have absence to signal absence.
There was a problem hiding this comment.
Also, it looks like its supposed to be nvmlast_updated_at_gte instead of list_updated_at_gte ?
| ?"token": has_more_list ? optional.of(body.pagination.nextCursor) : optional.none(), | ||
| }, | ||
| "has_more_timeline": false, | ||
| "list_updated_at_gte": has_more_list ? state.list_updated_at_gte : "", |
There was a problem hiding this comment.
| "list_updated_at_gte": has_more_list ? state.list_updated_at_gte : "", | |
| ?"list_updated_at_gte": has_more_list ? optional.of(state.list_updated_at_gte) : optional.none(), |
| "last_update_at": ( | ||
| [?state.?cursor.last_update_at] + updates | ||
| ).map(t, timestamp(t)).max().format(time_layout.RFC3339), | ||
| "events": (size(body.?data.orValue([])) > 0) ? |
There was a problem hiding this comment.
| "events": (size(body.?data.orValue([])) > 0) ? | |
| "events": body.?data[0].hasValue() ? |
| updates.size() > 0 ? | ||
| !has(state.worklist) ? // Exit early due to GET failure. | ||
| state | ||
| : (size(state.?worklist.?data.orValue([])) > 0) ? |
There was a problem hiding this comment.
| : (size(state.?worklist.?data.orValue([])) > 0) ? | |
| : state.?worklist.data[0].hasValue() ? |
| { | ||
| "error": { | ||
| "code": string(resp.StatusCode), | ||
| "id": string(resp.Status), |
There was a problem hiding this comment.
| "id": string(resp.Status), | |
| "id": resp.Status, |
| (size(resp.Body) != 0) ? | ||
| string(resp.Body) | ||
| : | ||
| string(resp.Status) + " (" + string(resp.StatusCode) + ")" |
There was a problem hiding this comment.
| string(resp.Status) + " (" + string(resp.StatusCode) + ")" | |
| resp.Status + " (" + string(resp.StatusCode) + ")" |
| "next_chain": {}, | ||
| "has_more_timeline": false, | ||
| "cursor": { | ||
| "last_update_at": threat.?threatInfo.updatedAt.orValue(state.?cursor.last_update_at.orValue("")), |
There was a problem hiding this comment.
Avoid "" for absence (also below).
1. Refactor the CEL code as per comments. 2. Updated system test as per the updated CEL code.
|
✅ All changelog entries have the correct PR link. |
|
No issues across the latest commits acd947c. Review summaryIssues found across earlier commits b0c46c5 — 1 medium, 1 low
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
| type: bool | ||
| title: Enable Threat Timeline Collection | ||
| description: >- | ||
| When enabled, each threat is followed by paginated requests to the threat timeline endpoint (`/web/api/v2.1/threats/{threat_id}/timeline`). For each timeline history entry, one document is emitted containing the full threat response with that entry nested under `timeline`. Threats whose timeline returns HTTP 200 with an empty `data` array emit a single threat document. If the timeline endpoint returns HTTP 404 (for example, missing permissions or an unavailable feature), the threat document is still emitted and collection advances to the next threat. Disable to collect threats from the list endpoint only. |
There was a problem hiding this comment.
Does missing permissions also cause 404? In CEL code, 404 emits a threat document, but other non-200 returns error. So just want to clarify
| "message": "Threat Detected: default.exe (malicious)", | ||
| "message": "STAR rule matched", |
There was a problem hiding this comment.
Looks like we are overriding previous value after adding timeline. This could break?
💚 Build Succeeded
History
|
Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally