Skip to content

[aws_bedrock] Surface Observe Mode Guardrail Policy Detections in Pipeline - #20470

Merged
mohitjha-elastic merged 2 commits into
elastic:mainfrom
mohitjha-elastic:aws_bedrock-1.6.0
Aug 4, 2026
Merged

[aws_bedrock] Surface Observe Mode Guardrail Policy Detections in Pipeline#20470
mohitjha-elastic merged 2 commits into
elastic:mainfrom
mohitjha-elastic:aws_bedrock-1.6.0

Conversation

@mohitjha-elastic

Copy link
Copy Markdown
Contributor

Proposed commit message

aws_bedrock: surface observe-mode guardrail policy detections in invocation pipeline

Surface guardrail policy detections from observe-mode evaluations in the invocation 
pipeline, including evaluations that do not intervene in the invocation.

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/aws_bedrock directory.
  • Run the following command to run tests.

elastic-package test -v

Related Issues

@mohitjha-elastic mohitjha-elastic self-assigned this Aug 3, 2026
@mohitjha-elastic
mohitjha-elastic requested review from a team as code owners August 3, 2026 11:45
@mohitjha-elastic mohitjha-elastic added enhancement New feature or request Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations] Integration:aws_bedrock Amazon Bedrock Team:SDE-Crest Crest developers on the Security Integrations team [elastic/sit-crest-contractors] labels Aug 3, 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 Aug 3, 2026

Copy link
Copy Markdown
Contributor

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

@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

✅ 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 ->

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: 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'
)) {

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: 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}}

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

@vera-review-bot

Copy link
Copy Markdown

Review summary

Issues found across the latest commits 37c4394 — 1 high, 2 medium
  • 🟠 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.* (link) (Unresolved)
  • 🟡 Observe-mode documents now get gen_ai.compliance.violation_code populated while gen_ai.compliance.violation_detected is left unset (link) (Unresolved)
  • 🟡 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 (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

cc @mohitjha-elastic

"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"

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.

Why are we calling this success? The request was denied.

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.

Good catch — the naming is easy to misread here.

This fixture is observe mode, not a blocked request:

  • stopReason is end_turn (not guardrail_intervened)
  • policy action is NONE (AWS observe/detect — no intervention); blocking would be BLOCKED
  • 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.

@mohitjha-elastic
mohitjha-elastic requested a review from efd6 August 4, 2026 07:38
@mergify

mergify Bot commented Aug 4, 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 cd848cc into elastic:main Aug 4, 2026
14 checks passed
@mohitjha-elastic
mohitjha-elastic deleted the aws_bedrock-1.6.0 branch August 4, 2026 08:39
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

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

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

Labels

enhancement New feature or request Integration:aws_bedrock Amazon Bedrock 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.

[aws_bedrock] Surface guardrail detections from observe-mode (non-intervened) evaluations

2 participants