Skip to content

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

Merged
mohitjha-elastic merged 6 commits into
elastic:mainfrom
mohitjha-elastic:sentinel_one-2.10.0
Jul 13, 2026
Merged

[sentinel_one] Add Support for Optional Threat Timeline Collection#19990
mohitjha-elastic merged 6 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: 1 suggestion found

💡 Suggestions (1): Optional style improvements. Apply when helpful.
File Line Rule Message
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 Jul 6, 2026
@mohitjha-elastic mohitjha-elastic changed the title [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
Contributor

🚀 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
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.

@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.

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 the wording here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the wording. Thanks!

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?

Comment on lines +62 to +69
{
"worklist": body,
"next_page": {
?"token": has_more_list ? optional.of(body.pagination.nextCursor) : optional.none(),
},
"has_more_timeline": false,
?"list_updated_at_gte": has_more_list ? optional.of(state.list_updated_at_gte) : optional.none(),
}

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.

All of this should be persisted into the cursor. See #20022.

1. Add cursor to store variables.
2. Fix overriding values for message ECS field.
3. Corrected wording for enable threat timeline collection description in manifest.
@mohitjha-elastic
mohitjha-elastic requested review from efd6 and kcreddy July 8, 2026 09:02
0NwJlyMBfayoPNcJKXrH/csJY7hbKviAHr1eYy9/8OL0dHf85FV+9uY5YndLcsDc
nygO9KTJuUiBrLr0AHEnqko=
-----END PRIVATE KEY-----
ssl: "enabled: true\nsupported_protocols:\n - TLSv1.2\ncipher_suites: \n - ECDHE-ECDSA-AES-128-CBC-SHA\n - ECDHE-ECDSA-AES-256-GCM-SHA384\ncurve_types:\n - P-256\ncertificate_authorities:\n - |\n -----BEGIN CERTIFICATE-----\n MIIDCjCCAfKgAwIBAgITJ706Mu2wJlKckpIvkWxEHvEyijANBgkqhkiG9w0BAQsF\n ADAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTkwNzIyMTkyOTA0WhgPMjExOTA2\n MjgxOTI5MDRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEB\n BQADggEPADCCAQoCggEBANce58Y/JykI58iyOXpxGfw0/gMvF0hUQAcUrSMxEO6n\n fZRA49b4OV4SwWmA3395uL2eB2NB8y8qdQ9muXUdPBWE4l9rMZ6gmfu90N5B5uEl\n 94NcfBfYOKi1fJQ9i7WKhTjlRkMCgBkWPkUokvBZFRt8RtF7zI77BSEorHGQCk9t\n /D7BS0GJyfVEhftbWcFEAG3VRcoMhF7kUzYwp+qESoriFRYLeDWv68ZOvG7eoWnP\n PsvZStEVEimjvK5NSESEQa9xWyJOmlOKXhkdymtcUd/nXnx6UTCFgnkgzSdTWV41\n CI6B6aJ9svCTI2QuoIq2HxX/ix7OvW1huVmcyHVxyUECAwEAAaNTMFEwHQYDVR0O\n BBYEFPwN1OceFGm9v6ux8G+DZ3TUDYxqMB8GA1UdIwQYMBaAFPwN1OceFGm9v6ux\n 8G+DZ3TUDYxqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAG5D\n 874A4YI7YUwOVsVAdbWtgp1d0zKcPRR+r2OdSbTAV5/gcS3jgBJ3i1BN34JuDVFw\n 3DeJSYT3nxy2Y56lLnxDeF8CUTUtVQx3CuGkRg1ouGAHpO/6OqOhwLLorEmxi7tA\n H2O8mtT0poX5AnOAhzVy7QW0D/k4WaoLyckM5hUa6RtvgvLxOwA0U+VGurCDoctu\n 8F4QOgTAWyh8EZIwaKCliFRSynDpv3JTUwtfZkxo6K6nce1RhCWFAsMvDZL8Dgc0\n yvgJ38BRsFOtkRuAGSf6ZUwTO8JJRRIFnpUzXflAnGivK9M13D5GEQMmIl6U9Pvk\n sxSmbIUfc2SGJGCJD4I=\n -----END CERTIFICATE-----\ncertificate: |\n -----BEGIN CERTIFICATE-----\n MIIDCjCCAfKgAwIBAgITJ706Mu2wJlKckpIvkWxEHvEyijANBgkqhkiG9w0BAQsF\n ADAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTkwNzIyMTkyOTA0WhgPMjExOTA2\n MjgxOTI5MDRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEB\n BQADggEPADCCAQoCggEBANce58Y/JykI58iyOXpxGfw0/gMvF0hUQAcUrSMxEO6n\n fZRA49b4OV4SwWmA3395uL2eB2NB8y8qdQ9muXUdPBWE4l9rMZ6gmfu90N5B5uEl\n 94NcfBfYOKi1fJQ9i7WKhTjlRkMCgBkWPkUokvBZFRt8RtF7zI77BSEorHGQCk9t\n /D7BS0GJyfVEhftbWcFEAG3VRcoMhF7kUzYwp+qESoriFRYLeDWv68ZOvG7eoWnP\n PsvZStEVEimjvK5NSESEQa9xWyJOmlOKXhkdymtcUd/nXnx6UTCFgnkgzSdTWV41\n CI6B6aJ9svCTI2QuoIq2HxX/ix7OvW1huVmcyHVxyUECAwEAAaNTMFEwHQYDVR0O\n BBYEFPwN1OceFGm9v6ux8G+DZ3TUDYxqMB8GA1UdIwQYMBaAFPwN1OceFGm9v6ux\n 8G+DZ3TUDYxqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAG5D\n 874A4YI7YUwOVsVAdbWtgp1d0zKcPRR+r2OdSbTAV5/gcS3jgBJ3i1BN34JuDVFw\n 3DeJSYT3nxy2Y56lLnxDeF8CUTUtVQx3CuGkRg1ouGAHpO/6OqOhwLLorEmxi7tA\n H2O8mtT0poX5AnOAhzVy7QW0D/k4WaoLyckM5hUa6RtvgvLxOwA0U+VGurCDoctu\n 8F4QOgTAWyh8EZIwaKCliFRSynDpv3JTUwtfZkxo6K6nce1RhCWFAsMvDZL8Dgc0\n yvgJ38BRsFOtkRuAGSf6ZUwTO8JJRRIFnpUzXflAnGivK9M13D5GEQMmIl6U9Pvk\n sxSmbIUfc2SGJGCJD4I=\n -----END CERTIFICATE-----\nkey: |\n -----BEGIN PRIVATE KEY-----\n MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXHufGPycpCOfI\n sjl6cRn8NP4DLxdIVEAHFK0jMRDup32UQOPW+DleEsFpgN9/ebi9ngdjQfMvKnUP\n Zrl1HTwVhOJfazGeoJn7vdDeQebhJfeDXHwX2DiotXyUPYu1ioU45UZDAoAZFj5F\n KJLwWRUbfEbRe8yO+wUhKKxxkApPbfw+wUtBicn1RIX7W1nBRABt1UXKDIRe5FM2\n MKfqhEqK4hUWC3g1r+vGTrxu3qFpzz7L2UrRFRIpo7yuTUhEhEGvcVsiTppTil4Z\n HcprXFHf5158elEwhYJ5IM0nU1leNQiOgemifbLwkyNkLqCKth8V/4sezr1tYblZ\n nMh1cclBAgMBAAECggEBAKdP5jyOicqknoG9/G564RcDsDyRt64NuO7I6hBg7SZx\n Jn7UKWDdFuFP/RYtoabn6QOxkVVlydp5Typ3Xu7zmfOyss479Q/HIXxmmbkD0Kp0\n eRm2KN3y0b6FySsS40KDRjKGQCuGGlNotW3crMw6vOvvsLTlcKgUHF054UVCHoK/\n Piz7igkDU7NjvJeha53vXL4hIjb10UtJNaGPxIyFLYRZdRPyyBJX7Yt3w8dgz8WM\n epOPu0dq3bUrY3WQXcxKZo6sQjE1h7kdl4TNji5jaFlvD01Y8LnyG0oThOzf0tve\n Gaw+kuy17gTGZGMIfGVcdeb+SlioXMAAfOps+mNIwTECgYEA/gTO8W0hgYpOQJzn\n BpWkic3LAoBXWNpvsQkkC3uba8Fcps7iiEzotXGfwYcb5Ewf5O3Lrz1EwLj7GTW8\n VNhB3gb7bGOvuwI/6vYk2/dwo84bwW9qRWP5hqPhNZ2AWl8kxmZgHns6WTTxpkRU\n zrfZ5eUrBDWjRU2R8uppgRImsxMCgYEA2MxuL/C/Ko0d7XsSX1kM4JHJiGpQDvb5\n GUrlKjP/qVyUysNF92B9xAZZHxxfPWpdfGGBynhw7X6s+YeIoxTzFPZVV9hlkpAA\n 5igma0n8ZpZEqzttjVdpOQZK8o/Oni/Q2S10WGftQOOGw5Is8+LY30XnLvHBJhO7\n TKMurJ4KCNsCgYAe5TDSVmaj3dGEtFC5EUxQ4nHVnQyCpxa8npL+vor5wSvmsfUF\n hO0s3GQE4sz2qHecnXuPldEd66HGwC1m2GKygYDk/v7prO1fQ47aHi9aDQB9N3Li\n e7Vmtdn3bm+lDjtn0h3Qt0YygWj+wwLZnazn9EaWHXv9OuEMfYxVgYKpdwKBgEze\n Zy8+WDm5IWRjn8cI5wT1DBT/RPWZYgcyxABrwXmGZwdhp3wnzU/kxFLAl5BKF22T\n kRZ+D+RVZvVutebE9c937BiilJkb0AXLNJwT9pdVLnHcN2LHHHronUhV7vetkop+\n kGMMLlY0lkLfoGq1AxpfSbIea9KZam6o6VKxEnPDAoGAFDCJm+ZtsJK9nE5GEMav\n NHy+PwkYsHhbrPl4dgStTNXLenJLIJ+Ke0Pcld4ZPfYdSyu/Tv4rNswZBNpNsW9K\n 0NwJlyMBfayoPNcJKXrH/csJY7hbKviAHr1eYy9/8OL0dHf85FV+9uY5YndLcsDc\n nygO9KTJuUiBrLr0AHEnqko=\n -----END PRIVATE KEY-----\n"

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/_dev/test/policy/test-all.yml:8

The ssl test var was reformatted from a multi-line YAML block scalar into a single escaped one-line string, an unrelated change that makes the test config hard to read; restore the block-scalar form.

Details

This PR adds optional threat timeline collection, but it also rewrites the ssl var in this policy test from a readable literal block scalar (ssl: | followed by indented YAML) into one long double-quoted string with embedded \n escapes. The two forms parse to the same value, so this is not a functional change, but the escaped one-liner is effectively unreadable and unrelated to the timeline feature. Unrelated, readability-reducing churn in a test fixture is worth reverting to keep the diff focused and the file maintainable.

Recommendation:

Restore the literal block-scalar form so the SSL config stays readable (cert/key bodies elided here):

  ssl: |
    enabled: true
    supported_protocols:
      - TLSv1.2
    cipher_suites:
      - ECDHE-ECDSA-AES-128-CBC-SHA
      - ECDHE-ECDSA-AES-256-GCM-SHA384
    curve_types:
      - P-256
    certificate_authorities:
      - |
        <localhost test CA PEM>
    certificate: |
      <localhost test certificate PEM>
    key: |
      <localhost test key PEM>

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

⚠️ Automated review — verify suggestions before applying.

Comment thread packages/sentinel_one/data_stream/threat/agent/stream/httpjson_as_cel.yml.hbs Outdated
Comment thread packages/sentinel_one/data_stream/threat/agent/stream/httpjson_as_cel.yml.hbs Outdated
Include list_updated_at_gte variable in cursor
@mohitjha-elastic
mohitjha-elastic requested a review from efd6 July 9, 2026 09:30
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

✅ All changelog entries have the correct PR link.

],
"cursor": state.cursor.with(
{
"worklist": {"data": tail(state.cursor.worklist.data)},

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: 🟠 High confidence: high path: packages/sentinel_one/data_stream/threat/agent/stream/httpjson_as_cel.yml.hbs:195

On a transient (non-404) timeline error the threat document is dropped, not just the enrichment: the branch emits only an error, advances the worklist with tail(), and bumps last_update_at, so the underlying threat is never emitted or re-collected. Emit the threat document on this branch too, like the 404 branch does.

Details

Timeline enrichment is an optional secondary call, but its failure handling is not best-effort for non-404 responses. When the timeline GET returns any status other than 200 or 404 (e.g. a transient 500/502/503 or a 429 rate-limit), this branch returns events containing only an error object and no threat message, while still advancing the worklist ("worklist": {"data": tail(state.cursor.worklist.data)}) and moving the list watermark forward (?"last_update_at": threat.?threatInfo.updatedAt.or(...)). The error-only event has error.message set with no message/event.original, so the pipeline's terminate processor discards it. Net result: the current threat is permanently lost, and because last_update_at was advanced past it, the next interval's list query (updatedAt__gte) will not return it again. The sibling 404 branch (lines 164-176) handles this correctly by emitting [{"message": threat.encode_json()}] before advancing, so a threat is never dropped merely because the timeline endpoint was unavailable. The two branches should be consistent: a failed best-effort enrichment must not discard the primary threat record.

Recommendation:

On the non-404 error branch, still emit the threat document (as the 404 branch does) so enrichment failure never drops the primary record. Optionally keep the error in a separate event for visibility:

:
  {
    "events": [
      {"message": threat.encode_json()},
      {
        "error": {
          "code": string(resp.StatusCode),
          "id": resp.Status,
          "message": "GET " + state.url.trim_right("/") + "/" + string(threat.id) + "/timeline: " + (
            (size(resp.Body) != 0) ?
              string(resp.Body)
            :
              resp.Status + " (" + string(resp.StatusCode) + ")"
          ),
        },
      },
    ],
    "cursor": state.cursor.with(
      {
        "worklist": {"data": tail(state.cursor.worklist.data)},
        "next_chain": {},
        "has_more_timeline": false,
        ?"last_update_at": threat.?threatInfo.updatedAt.or(state.?cursor.?last_update_at),
      }
    ),
    "want_more": size(state.cursor.worklist.data) > 1 || state.?cursor.?next_page.token.orValue(null) != null,
  }

Alternatively, if transient errors should be retried instead of skipped, do not tail() the worklist and do not advance last_update_at on this branch so the same threat is re-attempted on the next execution.


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

⚠️ Automated review — verify suggestions before applying.

@vera-review-bot

Copy link
Copy Markdown

Review summary

Issues found across the latest commits 3a7160f — 1 high
  • 🟠 On a transient (non-404) timeline error the threat document is dropped, not just the enrichment: the branch emits only an error, advances the worklist with tail(), and bumps last_update_at, so the underlying threat is never emitted or re-collected. Emit the threat document on this branch too, like the 404 branch does. (link) (Unresolved)
Issues found across earlier commits 5a9cf694a27717 (24 commits) — 1 low
  • 🔵 The ssl test var was reformatted from a multi-line YAML block scalar into a single escaped one-line string, an unrelated change that makes the test config hard to read (link) (Unresolved)
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.

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

cc @mohitjha-elastic

@mergify

mergify Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request
@mohitjha-elastic
mohitjha-elastic merged commit 282bf07 into elastic:main Jul 13, 2026
11 checks passed
@mohitjha-elastic
mohitjha-elastic deleted the sentinel_one-2.10.0 branch July 13, 2026 12:50
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

Package sentinel_one - 2.11.0 containing this change is available at https://epr.elastic.co/package/sentinel_one/2.11.0/

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]

3 participants