prisma_cloud: fix alert data stream dropping data on pagination and bad responses - #20917
prisma_cloud: fix alert data stream dropping data on pagination and bad responses#20917kcreddy wants to merge 5 commits into
Conversation
…ad responses
The alert data stream's CEL program could send an invalid request and
crash on non-JSON responses, causing successful API calls to yield no
indexed documents.
Two fixes:
- During pagination, when want_more was true but the cursor's
first_time_amount was null, the program emitted a literal
"&timeAmount=null" with no &timeUnit, producing an invalid request.
Fall back to the configured time_amount/time_unit, matching the
non-pagination branch.
- Guard the response decode: a synthetic 200 with an empty or
non-JSON (e.g. HTML 414) body no longer fails the program. Wrap the
decode in a StatusCode/try/is_error check and return {"events": []}
instead of crashing, preserving state for the next poll.
Add script tests for the alert data stream: an env smoke test and an
error_json_response test that interleaves HTML error pages with valid
alert responses and asserts a valid alert is still collected without a
spurious document. Both pass against a live stack.
✅ Elastic Docs Style Checker (Vale)No issues found on modified lines! 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. |
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
There was a problem hiding this comment.
Pull request overview
This PR updates the prisma_cloud integration to prevent the alert data stream’s CEL program from dropping data during pagination (invalid timeAmount=null URL) and from crashing on empty/non-JSON HTTP 200 bodies, and adds script tests to guard the behavior.
Changes:
- Fix pagination URL construction by falling back to configured
time_amount/time_unitwhencursor.first_time_amountis null. - Guard response decoding with
StatusCode == 200+try(...decode_json...)+is_error(...)to avoid CEL evaluation failures on non-JSON/empty bodies. - Add script tests (
env,error_json_response) and bump package version + changelog.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/prisma_cloud/manifest.yml | Bumps package version to 4.3.1. |
| packages/prisma_cloud/data_stream/alert/agent/stream/input.yml.hbs | Fixes pagination URL fallback and adds guarded JSON decoding for /v2/alert responses. |
| packages/prisma_cloud/data_stream/alert/_dev/test/scripts/error_json_response.txt | Adds regression script test for alternating HTML/non-JSON and valid JSON responses. |
| packages/prisma_cloud/data_stream/alert/_dev/test/scripts/env.txt | Adds basic env “smoke” script test. |
| packages/prisma_cloud/changelog.yml | Documents the 4.3.1 bugfix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| : | ||
| {"events": []} |
There was a problem hiding this comment.
The alternative is to make the auth token more robust; the documentation says that tokens live for 30min. So we can keep a token deadline next to the stored token and check that instead of abusing want_more.
There was a problem hiding this comment.
The data stream seems to use a different API: https://pan.dev/prisma-cloud/api/cspm/prisma-cloud-login-api-overview/ whose JWT is only valid for 10m. Updated accordingly.
🚀 Benchmarks reportTo see the full report comment with |
| }).do_request().as(resp, | ||
| bytes(resp.Body).decode_json().as(inner_body, { | ||
| (resp.StatusCode == 200) ? | ||
| try(bytes(resp.Body).decode_json()).as(inner_body, | ||
| is_error(inner_body.with({})) ? | ||
| {"events": []} | ||
| : |
There was a problem hiding this comment.
Move the status code condition to the line above and fix the indentation.
| try(bytes(resp.Body).decode_json()).as(inner_body, | ||
| is_error(inner_body.with({})) ? |
There was a problem hiding this comment.
try will never return an error; its evaluation will always be the result of the child evaluation or either an object or a string depending on the call signature.
| try(bytes(resp.Body).decode_json()).as(inner_body, | |
| is_error(inner_body.with({})) ? | |
| resp.Body.decode_json().as(inner_body, | |
| is_error(inner_body) ? |
| : | ||
| {"events": []} |
There was a problem hiding this comment.
The alternative is to make the auth token more robust; the documentation says that tokens live for 30min. So we can keep a token deadline next to the stored token and check that instead of abusing want_more.
| : | ||
| {"events": []} |
| ).do_request().as(resp, bytes(resp.Body).decode_json().as(body, { | ||
| ).do_request().as(resp, resp.Body.decode_json().as(body, { | ||
| "access_token": body.token, | ||
| "token_expiry": string(now + duration("9m")), |
There was a problem hiding this comment.
| "token_expiry": string(now + duration("9m")), | |
| "token_expiry": string(now() + duration("9m")), |
| { | ||
| "events": inner_body.items.map(e, { | ||
| "message": e.encode_json(), | ||
| }), |
There was a problem hiding this comment.
I think the indentation is wrong here.
| })) | ||
| }) | ||
| : | ||
| {"events": [], "token_expiry": string(now - duration("1m"))} |
There was a problem hiding this comment.
Why are we expiring this like this?
There was a problem hiding this comment.
Agreed. We can force the auth immediately here. Attempted now in de6a04b. Now on a non-200 we clear access_token and the login guard re-auths on an empty token, forcing a fresh login next poll if the token is rejected before the 9m deadline (early revocation / mid-pagination 401).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
packages/prisma_cloud/data_stream/alert/agent/stream/input.yml.hbs:131
- On non-200 responses, the code only clears
access_token. Because the rest of the pagination state (want_more,page_token,total_rows) is left untouched, a persistent error (e.g., 400 due to an invalid/expiredpageToken) can cause the stream to keep retrying the same failing paginated request indefinitely. Resetting pagination state here makes recovery deterministic.
{"events": [], "access_token": ""}
| ).do_request().as(resp, resp.Body.decode_json().as(body, { | ||
| "access_token": body.token, | ||
| "token_expiry": string(now() + duration("9m")), | ||
| })) |
|
✅ All changelog entries have the correct PR link. |
|
🟢 No issues across the latest commits de6a04b. Review summaryIssues found across earlier commits 0859951 — 1 high
🤖 AI-Generated Review | Vera Review Bot - v0.2.7 | 📚 Knowledge base: integration-skills
|
💚 Build Succeeded
History
cc @kcreddy |
| program: | | ||
| ( | ||
| state.with(has(state.want_more) && !(state.want_more) ? | ||
| state.with(!has(state.access_token) || state.access_token == "" || !has(state.token_expiry) || now() >= timestamp(state.token_expiry) ? |
There was a problem hiding this comment.
| state.with(!has(state.access_token) || state.access_token == "" || !has(state.token_expiry) || now() >= timestamp(state.token_expiry) ? | |
| state.with(state.?access_token.orValue("") != "" || state.?token_expiry.optMap(t, now() >= timestamp(t)).orValue(true) ? |
| ).do_request().as(resp, bytes(resp.Body).decode_json().as(body, { | ||
| ).do_request().as(resp, resp.Body.decode_json().as(body, { | ||
| "access_token": body.token, | ||
| "token_expiry": string(now() + duration("9m")), |
There was a problem hiding this comment.
I think maybe it would be worth parameterising this to make it obvious. It can be a constant string in the state decl.
|
Tick the box to add this pull request to the merge queue (same as
|
Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally
New script tests pass: