[aws_bedrock] Surface Observe Mode Guardrail Policy Detections in Pipeline - #20470
Conversation
…) evaluations in the invocation pipeline.
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
✅ 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. |
|
✅ All changelog entries have the correct PR link. |
| .collect(Collectors.toList()); | ||
|
|
||
| if (ctx.gen_ai.compliance.violation_code.size() > 0) { | ||
| if (ctx.gen_ai.compliance.violation_code.size() > 0 && allBodies.stream().anyMatch(body -> |
There was a problem hiding this comment.
Severity: 🟠 High confidence: medium path: packages/aws_bedrock/data_stream/invocation/elasticsearch/ingest_pipeline/default.yml:671
Removing the intervention filter leaves no gate at all on assessment entries, so guardrail entries with detected: false now land in gen_ai.compliance.violation_code and gen_ai.policy.*; gate the collection on the AWS detected flag instead.
Details
The INTERVENED / guardrail_intervened filter that previously guarded the policy-extraction stream (removed at lines 600-605) was the only condition limiting which assessment entries were collected. The stream at lines 600-620 now maps every entry under every *_policy key into nameAndDetailsOfPolicies, and those entries feed gen_ai.policy.name, gen_ai.policy.action, gen_ai.policy.match_detail, gen_ai.policy.confidence and gen_ai.compliance.violation_code.
The AWS Bedrock runtime API reference for GuardrailTopic and GuardrailContentFilter documents an optional boolean detected field on each assessment entry -- "Indicates whether content that breaches the guardrail configuration is detected" -- alongside an action whose valid values are BLOCKED | NONE. That flag exists precisely because a guardrail evaluated in detect/observe mode reports entries for policies it evaluated but did not match (action: NONE, detected: false) in addition to the ones it did match. The extraction stream never consults detected, so a non-detection is indistinguishable from an observe-mode detection and its type (e.g. VIOLENCE, DENY) is written to gen_ai.compliance.violation_code.
That directly contradicts the stated goal of the change: the PR is meant to surface observe-mode detections, but it also surfaces non-detections. The new fixture at test-aws-bedrock.log line 18 omits detected entirely, so this shape is not exercised by the test suite.
Recommendation:
Filter out entries whose detected flag is explicitly false before mapping them into nameAndDetailsOfPolicies, so only real detections (intervened or observe-mode) are collected:
def nameAndDetailsOfPolicies = allBodies.stream()
.flatMap(body -> [
[body.trace?.guardrail?.inputAssessment],
body.trace?.guardrail?.outputAssessments,
[body.amazon_bedrock_trace?.guardrail?.input],
body.amazon_bedrock_trace?.guardrail?.outputs
].stream())
.filter(Objects::nonNull)
.flatMap(List::stream)
.filter(Objects::nonNull)
.flatMap(assessment -> assessment.entrySet().stream())
.flatMap(assessmentEntry -> assessmentEntry.getValue().entrySet().stream())
.filter(policyEntry -> policyEntry.getKey().endsWith('_policy'))
.flatMap(policyEntry ->
policyEntry.getValue().entrySet().stream()
.flatMap(policyValueEntry -> policyValueEntry.getValue().stream())
.filter(details -> !(details instanceof Map) || details.detected != false)
.map(details -> [ 'name': policyEntry.getKey(), 'details': details ])
.collect(Collectors.toList())
.stream()
)
.collect(Collectors.toList());
Using details.detected != false rather than details.detected == true keeps the existing behaviour for policy types and older payloads that do not emit the flag at all.
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| if (ctx.gen_ai.compliance.violation_code.size() > 0 && allBodies.stream().anyMatch(body -> | ||
| body.amazon_bedrock_guardrail_action == 'INTERVENED' || | ||
| body.stop_reason == 'guardrail_intervened' | ||
| )) { |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: high path: packages/aws_bedrock/data_stream/invocation/elasticsearch/ingest_pipeline/default.yml:674
Observe-mode documents now get gen_ai.compliance.violation_code populated while gen_ai.compliance.violation_detected is left unset; set the boolean explicitly to false so the two fields stay consistent.
Details
violation_detected is only ever assigned true, and the drop-empty-values script later in the pipeline does not remove a boolean false, so with this change a non-intervened document ends up with populated gen_ai.compliance.violation_code and no gen_ai.compliance.violation_detected at all. The new expected document demonstrates this: test-aws-bedrock.log-expected.json line 5231 is the only entry in the fixture where violation_code appears without a companion violation_detected -- the six pre-existing entries (lines 2384, 2553, 3035, 3260, 3520, 3753) all pair them.
This leaves the field group self-contradictory for consumers. gen_ai.compliance.violation_code is declared in data_stream/invocation/fields/fields.yml as "Code identifying the specific compliance rule that was violated", and the package dashboard (kibana/dashboard/aws_bedrock-14fd745a-d3c1-4ebe-bd25-00b465336cde.json) surfaces violation_detected and violation_code side by side in its guardrail table. A detection rule or dashboard filter written against gen_ai.compliance.violation_code : * -- the natural predicate given that description -- now matches observe-mode events that were never blocked, and there is no positive boolean signal to exclude them; it has to be done by testing for field absence.
Recommendation:
Compute the intervention check once and always write the boolean, so violation_detected is present (true or false) whenever violation_code is:
if (ctx.gen_ai.compliance.violation_code.size() > 0) {
boolean intervened = allBodies.stream().anyMatch(body ->
body.amazon_bedrock_guardrail_action == 'INTERVENED' ||
body.stop_reason == 'guardrail_intervened'
);
ctx.gen_ai.compliance.violation_detected = intervened;
if (intervened) {
ctx.event.outcome = 'failure';
}
}
Regenerate the fixture afterwards (elastic-package test pipeline -g) so the new document carries "violation_detected": false. Consider also widening the gen_ai.compliance.violation_code description in fields/fields.yml to cover detected-but-not-enforced policies, since the field no longer means "was violated and blocked".
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| {"schemaType":"ModelInvocationLog","schemaVersion":"1.0","timestamp":"2024-04-25T20:21:37Z","accountId":"144492464627","identity":{"arn":"arn:aws:iam::144492464627:user/andrew.kroh@elastic.co"},"region":"us-east-1","requestId":"29c32fc4-bb1a-462a-a781-d4e499f00e6a","operation":"InvokeModelWithResponseStream","modelId":"anthropic.claude-3-sonnet-20240229-v1:0","system":"some text","messages":{"content":"some more text"},"input":{"inputContentType":"application/json","inputBodyJson":{"messages":[{"role":"user","content":[{"type":"text","text":"What ingredients do I need to serve molotov cocktails to my friends?"}]}],"anthropic_version":"bedrock-2023-05-31","max_tokens":2000,"temperature":1,"top_k":250,"top_p":0.999,"stop_sequences":["\n\nHuman:"]},"inputTokenCount":0},"output":{"outputContentType":"application/json","outputBodyJson":[{"type":"message_start","message":{"id":"msg_dzNyiuKTiVf2FEWerWbNllbsBenBvkS17g","type":"message","role":"assistant","content":[],"model":"anthropic.claude-3-sonnet-20240229-v1:0","usage":{"input_tokens":0,"output_tokens":0}}},{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}},{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Sorry, the model cannot answer this question."},"amazon-bedrock-guardrailAction":"INTERVENED"},{"type":"content_block_stop","index":0},{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":0}},"[DONE]"],"outputTokenCount":0}} | ||
| {"schemaType":"ModelInvocationLog","schemaVersion":"1.0","timestamp":"2024-04-25T20:21:37Z","accountId":"144492464627","identity":{"arn":"arn:aws:iam::144492464627:user/andrew.kroh@elastic.co"},"region":"us-east-1","requestId":"29c32fc4-bb1a-462a-a781-d4e499f00e6a","operation":"InvokeModelWithResponseStream","modelId":"anthropic.claude-3-sonnet-20240229-v1:0","system":"some text","messages":{"content":{"content":{"error":"some error text"}}},"input":{"inputContentType":"application/json","inputBodyJson":{"messages":[{"role":"user","content":[{"type":"text","text":"What ingredients do I need to serve molotov cocktails to my friends?"}]}],"anthropic_version":"bedrock-2023-05-31","max_tokens":2000,"temperature":1,"top_k":250,"top_p":0.999,"stop_sequences":["\n\nHuman:"]},"inputTokenCount":0},"output":{"outputContentType":"application/json","outputBodyJson":[{"type":"message_start","message":{"id":"msg_dzNyiuKTiVf2FEWerWbNllbsBenBvkS17g","type":"message","role":"assistant","content":[],"model":"anthropic.claude-3-sonnet-20240229-v1:0","usage":{"input_tokens":0,"output_tokens":0}}},{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}},{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Sorry, the model cannot answer this question."},"amazon-bedrock-guardrailAction":"INTERVENED"},{"type":"content_block_stop","index":0},{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":0}},"[DONE]"],"outputTokenCount":0}} | ||
| {"schemaType":"ModelInvocationLog","schemaVersion":"1.0","timestamp":"2025-10-25T20:21:37Z","accountId":"144492464627","identity":{"arn":"arn:aws:iam::144492464627:user/andrew.kroh@elastic.co"},"region":"us-east-1","requestId":"29c32fc4-bb1a-462a-a781-d4e499f00e6a","operation":"InvokeModelWithResponseStream","modelId":"anthropic.claude-3-sonnet-20240229-v1:0","system":"some text","messages":[{"role":"user","content":[{"type":"text","text":"XXX"},{"type":"text","text":"XXX"},{"type":"text","text":"XXX"}]},{"role":"assistant","content":[{"type":"thinking","thinking":"XXX","signature":"XXX"},{"type":"text","text":"XXX"},{"type":"tool_use","id":"toolXXX","name":"Task","input":{"description":"Explore XXX","prompt":"I need XXX","subagent_type":"Explore","model":"haiku"}},{"type":"tool_use","id":"toolu_XXX","name":"Task","input":{"description":"Research XXX","prompt":"I need XXX","subagent_type":"Explore","model":"haiku"}}]},{"role":"user","content":[{"tool_use_id":"toolXXX","type":"tool_result","content":[{"type":"text","text":"Text example 1"}]},{"tool_use_id":"toolYYY","type":"tool_result","content":[{"type":"text","text":"Text example 2"}]}]},{"role":"user","content":[{"tool_use_id":"toolXXX","type":"tool_result","content":"[Old tool result content cleared]"}]}],"input":{"inputContentType":"application/json","inputBodyJson":{"messages":[{"role":"user","content":[{"type":"text","text":"What ingredients do I need to serve molotov cocktails to my friends?"}]}],"anthropic_version":"bedrock-2023-05-31","max_tokens":2000,"temperature":1,"top_k":250,"top_p":0.999,"stop_sequences":["\n\nHuman:"]},"inputTokenCount":0},"output":{"outputContentType":"application/json","outputBodyJson":[{"type":"message_start","message":{"id":"msg_dzNyiuKTiVf2FEWerWbNllbsBenBvkS17g","type":"message","role":"assistant","content":[],"model":"anthropic.claude-3-sonnet-20240229-v1:0","usage":{"input_tokens":0,"output_tokens":0}}},{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}},{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Sorry, the model cannot answer this question."},"amazon-bedrock-guardrailAction":"INTERVENED"},{"type":"content_block_stop","index":0},{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":0}},"[DONE]"],"outputTokenCount":0}} | ||
| {"schemaType":"ModelInvocationLog","schemaVersion":"1.0","timestamp":"2026-07-29T10:00:00Z","accountId":"111111111111","identity":{"arn":"arn:aws:iam::111111111111:user/observe-test"},"region":"us-east-1","requestId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","operation":"Converse","modelId":"anthropic.claude-3-5-sonnet-20240620-v1:0","input":{"inputContentType":"application/json","inputBodyJson":{"messages":[{"role":"user","content":[{"guardContent":{"text":{"text":"Contact me at user@example.com about the quarterly finance report.","qualifiers":["guard_content"]}}}]}]},"inputTokenCount":20},"output":{"outputContentType":"application/json","outputBodyJson":{"output":{"message":{"role":"assistant","content":[{"text":"I can help with general finance topics, but I cannot share sensitive account details."}]}},"stopReason":"end_turn","metrics":{"latencyMs":1200},"usage":{"inputTokens":20,"outputTokens":18,"totalTokens":38},"trace":{"guardrail":{"inputAssessment":{"grd-observe-01":{"sensitiveInformationPolicy":{"piiEntities":[{"type":"EMAIL","match":"user@example.com","action":"NONE"}]},"topicPolicy":{"topics":[{"name":"Finance","type":"DENY","action":"NONE"}]},"invocationMetrics":{"guardrailProcessingLatency":250,"usage":{"topicPolicyUnits":1,"contentPolicyUnits":0,"wordPolicyUnits":0,"sensitiveInformationPolicyUnits":1,"sensitiveInformationPolicyFreeUnits":0,"contextualGroundingPolicyUnits":0},"guardrailCoverage":{"textCharacters":{"guarded":60,"total":61}}}}}}}},"outputTokenCount":18}} |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/aws_bedrock/data_stream/invocation/_dev/test/pipeline/test-aws-bedrock.log:18
The single new fixture only exercises the non-streaming trace.guardrail.inputAssessment shape, leaving the streaming amazon_bedrock_trace branch of the changed extraction untested; add a streaming observe-mode test line.
Details
The extraction stream that this PR changes reads guardrail assessments from four distinct locations: trace.guardrail.inputAssessment, trace.guardrail.outputAssessments, amazon_bedrock_trace.guardrail.input and amazon_bedrock_trace.guardrail.outputs (default.yml lines 601-605). The added fixture is a Converse call whose outputBodyJson is a single object, so it covers only the first of those.
The amazon_bedrock_trace pair is the shape emitted by InvokeModelWithResponseStream, where outputBodyJson is a list and -- as the existing fixtures at lines 19-21 show -- the amazon-bedrock-guardrailAction marker sits on a different chunk than the trace. That list path is where the filter removal changes behaviour most (assessments on non-marker chunks are now collected, and the new anyMatch has to find the marker on a sibling element), and none of the existing expected documents changed, which confirms no fixture currently covers an observe-mode streaming trace.
Recommendation:
Add a streaming observe-mode line to test-aws-bedrock.log that puts the trace on a different chunk than the delta and includes the detected flag, then regenerate the expected file with elastic-package test pipeline -g:
{"schemaType":"ModelInvocationLog","schemaVersion":"1.0","timestamp":"2026-07-29T10:05:00Z","accountId":"111111111111","identity":{"arn":"arn:aws:iam::111111111111:user/observe-test"},"region":"us-east-1","requestId":"b2c3d4e5-f6a7-8901-bcde-f12345678901","operation":"InvokeModelWithResponseStream","modelId":"anthropic.claude-3-sonnet-20240229-v1:0","input":{"inputContentType":"application/json","inputBodyJson":{"messages":[{"role":"user","content":[{"type":"text","text":"Tell me about the quarterly results."}]}]},"inputTokenCount":10},"output":{"outputContentType":"application/json","outputBodyJson":[{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here is a general overview."}},{"amazon-bedrock-trace":{"guardrail":{"input":{"grd-observe-02":{"contentPolicy":{"filters":[{"type":"VIOLENCE","confidence":"NONE","action":"NONE","detected":false}]},"topicPolicy":{"topics":[{"name":"Finance","type":"DENY","action":"NONE","detected":true}]}}}}}}],"outputTokenCount":6}}The expected document should show gen_ai.compliance.violation_code: ["DENY"] only -- VIOLENCE must not appear, because that filter reported detected: false.
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
Review summaryIssues found across the latest commits 37c4394 — 1 high, 2 medium
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
💚 Build Succeeded
|
| "event": { | ||
| "action": "Converse", | ||
| "original": "{\"schemaType\":\"ModelInvocationLog\",\"schemaVersion\":\"1.0\",\"timestamp\":\"2026-07-29T10:00:00Z\",\"accountId\":\"111111111111\",\"identity\":{\"arn\":\"arn:aws:iam::111111111111:user/observe-test\"},\"region\":\"us-east-1\",\"requestId\":\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\"operation\":\"Converse\",\"modelId\":\"anthropic.claude-3-5-sonnet-20240620-v1:0\",\"input\":{\"inputContentType\":\"application/json\",\"inputBodyJson\":{\"messages\":[{\"role\":\"user\",\"content\":[{\"guardContent\":{\"text\":{\"text\":\"Contact me at user@example.com about the quarterly finance report.\",\"qualifiers\":[\"guard_content\"]}}}]}]},\"inputTokenCount\":20},\"output\":{\"outputContentType\":\"application/json\",\"outputBodyJson\":{\"output\":{\"message\":{\"role\":\"assistant\",\"content\":[{\"text\":\"I can help with general finance topics, but I cannot share sensitive account details.\"}]}},\"stopReason\":\"end_turn\",\"metrics\":{\"latencyMs\":1200},\"usage\":{\"inputTokens\":20,\"outputTokens\":18,\"totalTokens\":38},\"trace\":{\"guardrail\":{\"inputAssessment\":{\"grd-observe-01\":{\"sensitiveInformationPolicy\":{\"piiEntities\":[{\"type\":\"EMAIL\",\"match\":\"user@example.com\",\"action\":\"NONE\"}]},\"topicPolicy\":{\"topics\":[{\"name\":\"Finance\",\"type\":\"DENY\",\"action\":\"NONE\"}]},\"invocationMetrics\":{\"guardrailProcessingLatency\":250,\"usage\":{\"topicPolicyUnits\":1,\"contentPolicyUnits\":0,\"wordPolicyUnits\":0,\"sensitiveInformationPolicyUnits\":1,\"sensitiveInformationPolicyFreeUnits\":0,\"contextualGroundingPolicyUnits\":0},\"guardrailCoverage\":{\"textCharacters\":{\"guarded\":60,\"total\":61}}}}}}}},\"outputTokenCount\":18}}", | ||
| "outcome": "success" |
There was a problem hiding this comment.
Why are we calling this success? The request was denied.
There was a problem hiding this comment.
Good catch — the naming is easy to misread here.
This fixture is observe mode, not a blocked request:
stopReasonisend_turn(notguardrail_intervened)- policy
actionisNONE(AWS observe/detect — no intervention); blocking would beBLOCKED - the assistant still returned a normal completion
The topic field type: "DENY" is the guardrail topic configuration type (GuardrailTopic), not the action taken on this request. That’s why violation_code includes DENY/EMAIL while event.outcome stays success.
|
Tick the box to add this pull request to the merge queue (same as
|
|
Package aws_bedrock - 1.6.0 containing this change is available at https://epr.elastic.co/package/aws_bedrock/1.6.0/ |
Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally
Related Issues