[sentinel_one] Add Support for Optional Threat Timeline Collection - #19990
Conversation
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
Elastic Docs Style Checker (Vale)Summary: 1 suggestion found 💡 Suggestions (1): 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.
| 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 the wording here.
There was a problem hiding this comment.
Updated the wording. Thanks!
| "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?
| { | ||
| "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(), | ||
| } |
There was a problem hiding this comment.
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.
…sentinel_one-2.10.0
| 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" |
There was a problem hiding this comment.
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.
Include list_updated_at_gte variable in cursor
|
✅ All changelog entries have the correct PR link. |
| ], | ||
| "cursor": state.cursor.with( | ||
| { | ||
| "worklist": {"data": tail(state.cursor.worklist.data)}, |
There was a problem hiding this comment.
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.
Review summaryIssues found across the latest commits 3a7160f — 1 high
Issues found across earlier commits 5a9cf69…4a27717 (24 commits) — 1 low
Issues found across earlier commits b0c46c5 — 1 medium, 1 low
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
💚 Build Succeeded
History
|
|
Tick the box to add this pull request to the merge queue (same as
|
|
Package sentinel_one - 2.11.0 containing this change is available at https://epr.elastic.co/package/sentinel_one/2.11.0/ |
Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally