Skip to content

[sentinel_one] Add Support for Optional Threat Timeline Collection#19990

Open
mohitjha-elastic wants to merge 3 commits into
elastic:mainfrom
mohitjha-elastic:sentinel_one-2.10.0
Open

[sentinel_one] Add Support for Optional Threat Timeline Collection#19990
mohitjha-elastic wants to merge 3 commits into
elastic:mainfrom
mohitjha-elastic:sentinel_one-2.10.0

Conversation

@mohitjha-elastic

Copy link
Copy Markdown
Contributor

Proposed commit message

sentinel_one: Adds optional threat timeline collection to the threat data stream

This change adds optional threat timeline collection to the SentinelOne threat data stream.
When enabled, the integration retrieves paginated timeline entries for each threat and emits
each entry as a separate event under the `sentinel_one.threat.timeline.*` fields.
Additionally, STAR rule timeline entries are mapped to the ECS `rule.id`, `rule.name`, and
`rule.description` fields to provide enriched detection metadata.

Checklist

  • I have reviewed tips for building integrations and this pull request is aligned with them.
  • I have verified that all data streams collect metrics or logs.
  • I have added an entry to my package's changelog.yml file.
  • I have verified that Kibana version constraints are current according to guidelines.
  • I have verified that any added dashboard complies with Kibana's Dashboard good practices

How to test this PR locally

  • Clone integrations repo.
  • Install the elastic package locally.
  • Start the elastic stack using the elastic package.
  • Move to integrations/packages/sentinel_one directory.
  • Run the following command to run tests.

elastic-package test -v

@mohitjha-elastic mohitjha-elastic self-assigned this Jul 6, 2026
@mohitjha-elastic mohitjha-elastic requested review from a team as code owners July 6, 2026 10:18
@mohitjha-elastic mohitjha-elastic added documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. enhancement New feature or request Integration:sentinel_one SentinelOne Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations] Team:SDE-Crest Crest developers on the Security Integrations team [elastic/sit-crest-contractors] labels Jul 6, 2026
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/security-service-integrations (Team:Security-Service Integrations)

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Elastic Docs Style Checker (Vale)

Summary: 2 suggestions found

💡 Suggestions (2): Optional style improvements. Apply when helpful.
File Line Rule Message
packages/sentinel_one/data_stream/threat/manifest.yml 29 Elastic.WordChoice Consider using 'deactivate, deselect, hide, turn off' instead of 'Disable', unless the term is in the UI.
packages/sentinel_one/data_stream/threat/manifest.yml 38 Elastic.WordChoice Consider using 'can, might' instead of 'may', unless the term is in the UI.

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.

@mohitjha-elastic mohitjha-elastic changed the title Add support for optional threat timeline collection [sentinel_one] Add support for Optional Threat Timeline Collection Jul 6, 2026
@mohitjha-elastic mohitjha-elastic changed the title [sentinel_one] Add support for Optional Threat Timeline Collection [sentinel_one] Add Support for Optional Threat Timeline Collection Jul 6, 2026
"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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@elastic-vault-github-plugin-prod

Copy link
Copy Markdown

🚀 Benchmarks report

To see the full report comment with /test benchmark fullreport

(
state.?want_more.orValue(false) ?
state.list_updated_at_gte
(size(state.?worklist.?data.orValue([])) > 0) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(size(state.?worklist.?data.orValue([])) > 0) ?
state.?worklist.data[0].hasValue() ?

Comment on lines +38 to +41
"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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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.

@kcreddy kcreddy Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it looks like its supposed to be last_updated_at_gte instead of list_updated_at_gte ? nvm

?"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 : "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: (size(state.?worklist.?data.orValue([])) > 0) ?
: state.?worklist.data[0].hasValue() ?

{
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"id": string(resp.Status),
"id": resp.Status,

(size(resp.Body) != 0) ?
string(resp.Body)
:
string(resp.Status) + " (" + string(resp.StatusCode) + ")"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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("")),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid "" for absence (also below).

1. Refactor the CEL code as per comments.
2. Updated system test as per the updated CEL code.
@mohitjha-elastic mohitjha-elastic requested a review from efd6 July 7, 2026 09:44
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown

✅ All changelog entries have the correct PR link.

@vera-review-bot

Copy link
Copy Markdown

No issues across the latest commits acd947c.

Review summary

Issues found across earlier commits b0c46c5 — 1 medium, 1 low
  • 🟡 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. (link) (Unresolved)
  • 🔵 The empty-worklist branch returns want_more: true with an empty events array (link) (Unresolved)

A new commit triggers another review — at most once every 15 minutes. I skip the PR while it's approved or has merge conflicts.

🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines -76 to +75
"message": "Threat Detected: default.exe (malicious)",
"message": "STAR rule matched",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we are overriding previous value after adding timeline. This could break?

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

cc @mohitjha-elastic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. enhancement New feature or request Integration:sentinel_one SentinelOne Team:SDE-Crest Crest developers on the Security Integrations team [elastic/sit-crest-contractors] Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants