fix: 12152 - Redacted sensitive information logged in bedrock guardrails (#13356)

This commit is contained in:
Anand Khinvasara 2025-08-07 08:42:11 -07:00 • committed by GitHub
parent 30fc5b871c
commit 96dca4eff8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 909 additions and 1 deletions

View file

@ -5,6 +5,7 @@
# +-------------------------------------------------------------+
# Thank you users! We ❤️ you! - Krrish & Ishaan
import copy
import os
import sys
@ -50,6 +51,51 @@ from litellm.types.utils import (
GUARDRAIL_NAME = "bedrock"
def _redact_pii_matches(response_json: dict) -> dict:
try:
# Create a deep copy to avoid modifying the original response
redacted_response = copy.deepcopy(response_json)
# Get assessments from the response
assessments = redacted_response.get("assessments", [])
if not assessments:
return redacted_response
for assessment in assessments:
# Redact PII entities in sensitive information policy
sensitive_info_policy = assessment.get("sensitiveInformationPolicy")
if sensitive_info_policy:
pii_entities = sensitive_info_policy.get("piiEntities", [])
for pii_entity in pii_entities:
if "match" in pii_entity:
pii_entity["match"] = "[REDACTED]"
# Redact regex matches
regexes = sensitive_info_policy.get("regexes", [])
for regex_match in regexes:
if "match" in regex_match:
regex_match["match"] = "[REDACTED]"
# Redact custom word matches in word policy
word_policy = assessment.get("wordPolicy")
if word_policy:
custom_words = word_policy.get("customWords", [])
for custom_word in custom_words:
if "match" in custom_word:
custom_word["match"] = "[REDACTED]"
managed_words = word_policy.get("managedWordLists", [])
for managed_word in managed_words:
if "match" in managed_word:
managed_word["match"] = "[REDACTED]"
return redacted_response
except Exception as e:
# We do not want to fail in any case so this is just a warning
verbose_proxy_logger.warning("Guardrail log redaction failed: %s", str(e))
return response_json
class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
def __init__(
self,
@ -271,10 +317,11 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
data=prepared_request.body, # type: ignore
headers=prepared_request.headers, # type: ignore
)
verbose_proxy_logger.debug("Bedrock AI response: %s", response.text)
if response.status_code == 200:
# check if the response was flagged
_json_response = response.json()
redacted_response = _redact_pii_matches(_json_response)
verbose_proxy_logger.debug("Bedrock AI response : %s", redacted_response)
bedrock_guardrail_response = BedrockGuardrailResponse(**_json_response)
if self._should_raise_guardrail_blocked_exception(
bedrock_guardrail_response

View file

@ -0,0 +1,861 @@
"""
Unit tests for Bedrock Guardrails
"""
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(0, os.path.abspath("../../../../../.."))
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
BedrockGuardrail,
_redact_pii_matches,
)
@pytest.mark.asyncio
async def test__redact_pii_matches_function():
"""Test the _redact_pii_matches function directly"""
# Test case 1: Response with PII entities
response_with_pii = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{"type": "NAME", "match": "John Smith", "action": "BLOCKED"},
{
"type": "US_SOCIAL_SECURITY_NUMBER",
"match": "324-12-3212",
"action": "BLOCKED",
},
{"type": "PHONE", "match": "607-456-7890", "action": "BLOCKED"},
]
}
}
],
"outputs": [{"text": "Input blocked by PII policy"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_with_pii)
# Verify that PII matches are redacted
pii_entities = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
]
assert pii_entities[0]["match"] == "[REDACTED]", "Name should be redacted"
assert pii_entities[1]["match"] == "[REDACTED]", "SSN should be redacted"
assert pii_entities[2]["match"] == "[REDACTED]", "Phone should be redacted"
# Verify other fields remain unchanged
assert pii_entities[0]["type"] == "NAME"
assert pii_entities[1]["type"] == "US_SOCIAL_SECURITY_NUMBER"
assert pii_entities[2]["type"] == "PHONE"
assert redacted_response["action"] == "GUARDRAIL_INTERVENED"
assert redacted_response["outputs"][0]["text"] == "Input blocked by PII policy"
print("PII redaction function test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_no_pii():
"""Test _redact_pii_matches with response that has no PII"""
response_no_pii = {"action": "NONE", "assessments": [], "outputs": []}
# Call the redaction function
redacted_response = _redact_pii_matches(response_no_pii)
# Should return the same response unchanged
assert redacted_response == response_no_pii
print("No PII redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_empty_assessments():
"""Test _redact_pii_matches with empty assessments"""
response_empty_assessments = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [{"sensitiveInformationPolicy": {"piiEntities": []}}],
"outputs": [{"text": "Some output"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_empty_assessments)
# Should return the same response unchanged
assert redacted_response == response_empty_assessments
print("Empty assessments redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_malformed_response():
"""Test _redact_pii_matches with malformed response (should not crash)"""
# Test with completely malformed response
malformed_response = {
"action": "GUARDRAIL_INTERVENED",
"assessments": "not_a_list", # This should cause an exception
}
# Should not crash and return original response
redacted_response = _redact_pii_matches(malformed_response)
assert redacted_response == malformed_response
# Test with missing keys
missing_keys_response = {
"action": "GUARDRAIL_INTERVENED"
# Missing assessments key
}
redacted_response = _redact_pii_matches(missing_keys_response)
assert redacted_response == missing_keys_response
print("Malformed response redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_multiple_assessments():
"""Test _redact_pii_matches with multiple assessments containing PII"""
response_multiple_assessments = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "EMAIL",
"match": "john@example.com",
"action": "ANONYMIZED",
}
]
}
},
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "CREDIT_DEBIT_CARD_NUMBER",
"match": "1234-5678-9012-3456",
"action": "BLOCKED",
},
{
"type": "ADDRESS",
"match": "123 Main St, Anytown USA",
"action": "ANONYMIZED",
},
]
}
},
],
"outputs": [{"text": "Multiple PII detected"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_multiple_assessments)
# Verify all PII in all assessments are redacted
assessment1_pii = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
]
assessment2_pii = redacted_response["assessments"][1]["sensitiveInformationPolicy"][
"piiEntities"
]
assert assessment1_pii[0]["match"] == "[REDACTED]", "Email should be redacted"
assert assessment2_pii[0]["match"] == "[REDACTED]", "Credit card should be redacted"
assert assessment2_pii[1]["match"] == "[REDACTED]", "Address should be redacted"
# Verify types remain unchanged
assert assessment1_pii[0]["type"] == "EMAIL"
assert assessment2_pii[0]["type"] == "CREDIT_DEBIT_CARD_NUMBER"
assert assessment2_pii[1]["type"] == "ADDRESS"
print("Multiple assessments redaction test passed")
@pytest.mark.asyncio
async def test_bedrock_guardrail_logging_uses_redacted_response():
"""Test that the Bedrock guardrail uses redacted response for logging"""
# Create proper mock objects
mock_user_api_key_dict = UserAPIKeyAuth()
guardrail = BedrockGuardrail(
guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT"
)
# Mock the Bedrock API response with PII
mock_bedrock_response = MagicMock()
mock_bedrock_response.status_code = 200
mock_bedrock_response.json.return_value = {
"action": "GUARDRAIL_INTERVENED",
"outputs": [{"text": "Hello, my phone number is {PHONE}"}],
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "PHONE",
"match": "+1 412 555 1212", # This should be redacted in logs
"action": "ANONYMIZED",
}
]
}
}
],
}
request_data = {
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello, my phone number is +1 412 555 1212"},
],
}
# Mock AWS credentials to avoid credential loading issues in CI
mock_credentials = MagicMock()
mock_credentials.access_key = "test-access-key"
mock_credentials.secret_key = "test-secret-key"
mock_credentials.token = None
# Mock AWS-related methods to ensure test runs without external dependencies
with patch.object(
guardrail.async_handler, "post", new_callable=AsyncMock
) as mock_post, patch(
"litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails.verbose_proxy_logger.debug"
) as mock_debug, patch.object(
guardrail, "_load_credentials", return_value=(mock_credentials, "us-east-1")
) as mock_load_creds, patch.object(
guardrail, "_prepare_request", return_value=MagicMock()
) as mock_prepare_request:
mock_post.return_value = mock_bedrock_response
# Call the method that should log the redacted response
await guardrail.make_bedrock_api_request(
source="INPUT",
messages=request_data.get("messages"),
request_data=request_data,
)
# Verify that debug logging was called
mock_debug.assert_called()
# Get the logged response (second argument to debug call)
logged_calls = mock_debug.call_args_list
bedrock_response_log_call = None
for call in logged_calls:
args, kwargs = call
if len(args) >= 2 and "Bedrock AI response" in str(args[0]):
bedrock_response_log_call = call
break
assert (
bedrock_response_log_call is not None
), "Should have logged Bedrock AI response"
# Extract the logged response data
logged_response = bedrock_response_log_call[0][
1
] # Second argument to debug call
# Verify that the logged response has redacted PII
assert (
logged_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
][0]["match"]
== "[REDACTED]"
)
# Verify other fields are preserved
assert logged_response["action"] == "GUARDRAIL_INTERVENED"
assert (
logged_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
][0]["type"]
== "PHONE"
)
print("Bedrock guardrail logging redaction test passed")
@pytest.mark.asyncio
async def test_bedrock_guardrail_original_response_not_modified():
"""Test that the original response is not modified by redaction, only the logged version"""
# Create proper mock objects
mock_user_api_key_dict = UserAPIKeyAuth()
guardrail = BedrockGuardrail(
guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT"
)
# Mock the Bedrock API response with PII
original_response_data = {
"action": "GUARDRAIL_INTERVENED",
"outputs": [{"text": "Hello, my phone number is {PHONE}"}],
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "PHONE",
"match": "+1 412 555 1212", # This should NOT be modified in original
"action": "ANONYMIZED",
}
]
}
}
],
}
mock_bedrock_response = MagicMock()
mock_bedrock_response.status_code = 200
mock_bedrock_response.json.return_value = original_response_data
request_data = {
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello, my phone number is +1 412 555 1212"},
],
}
# Mock AWS credentials to avoid credential loading issues in CI
mock_credentials = MagicMock()
mock_credentials.access_key = "test-access-key"
mock_credentials.secret_key = "test-secret-key"
mock_credentials.token = None
# Mock AWS-related methods to ensure test runs without external dependencies
with patch.object(
guardrail.async_handler, "post", new_callable=AsyncMock
) as mock_post, patch.object(
guardrail, "_load_credentials", return_value=(mock_credentials, "us-east-1")
) as mock_load_creds, patch.object(
guardrail, "_prepare_request", return_value=MagicMock()
) as mock_prepare_request:
mock_post.return_value = mock_bedrock_response
# Call the method
result = await guardrail.make_bedrock_api_request(
source="INPUT",
messages=request_data.get("messages"),
request_data=request_data,
)
# Verify that the original response data was not modified
# (The json() method should return the original data)
original_data = mock_bedrock_response.json()
assert (
original_data["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
][0]["match"]
== "+1 412 555 1212"
)
# Verify that the returned BedrockGuardrailResponse contains original data
assert (
result["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][
"match"
]
== "+1 412 555 1212"
)
print("Original response not modified test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_preserves_non_pii_entities():
"""Test that _redact_pii_matches only affects PII-related entities and preserves other assessment data"""
response_with_mixed_data = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "EMAIL",
"match": "user@example.com",
"action": "ANONYMIZED",
"confidence": "HIGH",
}
],
"regexes": [
{
"name": "custom_pattern",
"match": "some_pattern_match",
"action": "BLOCKED",
}
],
},
"contentPolicy": {
"filters": [
{
"type": "VIOLENCE",
"confidence": "MEDIUM",
"action": "BLOCKED",
}
]
},
"topicPolicy": {
"topics": [
{
"name": "Restricted Topic",
"type": "DENY",
"action": "BLOCKED",
}
]
},
}
],
"outputs": [{"text": "Content blocked"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_with_mixed_data)
# Verify that PII entity matches are redacted
pii_entities = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
]
assert pii_entities[0]["match"] == "[REDACTED]", "PII match should be redacted"
assert pii_entities[0]["type"] == "EMAIL", "PII type should be preserved"
assert pii_entities[0]["action"] == "ANONYMIZED", "PII action should be preserved"
assert pii_entities[0]["confidence"] == "HIGH", "PII confidence should be preserved"
# Verify that regex matches are also redacted (updated behavior)
regexes = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"regexes"
]
assert regexes[0]["match"] == "[REDACTED]", "Regex match should be redacted"
assert regexes[0]["name"] == "custom_pattern", "Regex name should be preserved"
assert regexes[0]["action"] == "BLOCKED", "Regex action should be preserved"
# Verify that other policies are completely unchanged
content_policy = redacted_response["assessments"][0]["contentPolicy"]
assert content_policy["filters"][0]["type"] == "VIOLENCE"
assert content_policy["filters"][0]["confidence"] == "MEDIUM"
topic_policy = redacted_response["assessments"][0]["topicPolicy"]
assert topic_policy["topics"][0]["name"] == "Restricted Topic"
# Verify top-level fields are unchanged
assert redacted_response["action"] == "GUARDRAIL_INTERVENED"
assert redacted_response["outputs"][0]["text"] == "Content blocked"
print("Preserves non-PII entities test passed")
@pytest.mark.asyncio
async def test_pii_redaction_matches_debug_output_format():
"""Test that demonstrates the exact behavior shown in your debug output"""
# This matches the structure from your debug output
original_response = {
"action": "GUARDRAIL_INTERVENED",
"actionReason": "Guardrail blocked.",
"assessments": [
{
"invocationMetrics": {
"guardrailCoverage": {
"textCharacters": {"guarded": 84, "total": 84}
},
"guardrailProcessingLatency": 322,
"usage": {
"contentPolicyImageUnits": 0,
"contentPolicyUnits": 0,
"contextualGroundingPolicyUnits": 0,
"sensitiveInformationPolicyFreeUnits": 0,
"sensitiveInformationPolicyUnits": 1,
"topicPolicyUnits": 0,
"wordPolicyUnits": 0,
},
},
"sensitiveInformationPolicy": {
"piiEntities": [
{
"action": "BLOCKED",
"detected": True,
"match": "John Smith",
"type": "NAME",
},
{
"action": "BLOCKED",
"detected": True,
"match": "324-12-3212",
"type": "US_SOCIAL_SECURITY_NUMBER",
},
{
"action": "BLOCKED",
"detected": True,
"match": "607-456-7890",
"type": "PHONE",
},
]
},
}
],
"blockedResponse": "Input blocked by PII policy",
"guardrailCoverage": {"textCharacters": {"guarded": 84, "total": 84}},
"output": [{"text": "Input blocked by PII policy"}],
"outputs": [{"text": "Input blocked by PII policy"}],
"usage": {
"contentPolicyImageUnits": 0,
"contentPolicyUnits": 0,
"contextualGroundingPolicyUnits": 0,
"sensitiveInformationPolicyFreeUnits": 0,
"sensitiveInformationPolicyUnits": 1,
"topicPolicyUnits": 0,
"wordPolicyUnits": 0,
},
}
# Apply redaction
redacted_response = _redact_pii_matches(original_response)
# Verify the redacted response matches your expected debug output
pii_entities = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"piiEntities"
]
# All PII matches should be redacted
assert pii_entities[0]["match"] == "[REDACTED]", "NAME should be redacted"
assert pii_entities[1]["match"] == "[REDACTED]", "SSN should be redacted"
assert pii_entities[2]["match"] == "[REDACTED]", "PHONE should be redacted"
# But all other fields should be preserved
assert pii_entities[0]["type"] == "NAME"
assert pii_entities[1]["type"] == "US_SOCIAL_SECURITY_NUMBER"
assert pii_entities[2]["type"] == "PHONE"
assert pii_entities[0]["action"] == "BLOCKED"
assert pii_entities[0]["detected"] == True
# Verify that the original response is unchanged
original_pii_entities = original_response["assessments"][0][
"sensitiveInformationPolicy"
]["piiEntities"]
assert (
original_pii_entities[0]["match"] == "John Smith"
), "Original should be unchanged"
assert (
original_pii_entities[1]["match"] == "324-12-3212"
), "Original should be unchanged"
assert (
original_pii_entities[2]["match"] == "607-456-7890"
), "Original should be unchanged"
# Verify all other metadata is preserved in redacted response
assert redacted_response["action"] == "GUARDRAIL_INTERVENED"
assert redacted_response["actionReason"] == "Guardrail blocked."
assert redacted_response["blockedResponse"] == "Input blocked by PII policy"
assert (
redacted_response["assessments"][0]["invocationMetrics"][
"guardrailProcessingLatency"
]
== 322
)
print("PII redaction matches debug output format test passed")
print(
f"Original PII values preserved: {[e['match'] for e in original_pii_entities]}"
)
print(f"Redacted PII values: {[e['match'] for e in pii_entities]}")
@pytest.mark.asyncio
async def test__redact_pii_matches_with_regex_matches():
"""Test redaction of regex matches in sensitive information policy"""
response_with_regex = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"sensitiveInformationPolicy": {
"regexes": [
{
"name": "SSN_PATTERN",
"match": "123-45-6789",
"action": "BLOCKED",
},
{
"name": "CREDIT_CARD_PATTERN",
"match": "4111-1111-1111-1111",
"action": "ANONYMIZED",
},
]
}
}
],
"outputs": [{"text": "Regex patterns detected"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_with_regex)
# Verify that regex matches are redacted
regexes = redacted_response["assessments"][0]["sensitiveInformationPolicy"][
"regexes"
]
assert regexes[0]["match"] == "[REDACTED]", "SSN regex match should be redacted"
assert (
regexes[1]["match"] == "[REDACTED]"
), "Credit card regex match should be redacted"
# Verify other fields are preserved
assert regexes[0]["name"] == "SSN_PATTERN", "Regex name should be preserved"
assert regexes[0]["action"] == "BLOCKED", "Regex action should be preserved"
assert regexes[1]["name"] == "CREDIT_CARD_PATTERN", "Regex name should be preserved"
assert regexes[1]["action"] == "ANONYMIZED", "Regex action should be preserved"
# Verify original response is unchanged
original_regexes = response_with_regex["assessments"][0][
"sensitiveInformationPolicy"
]["regexes"]
assert original_regexes[0]["match"] == "123-45-6789", "Original should be unchanged"
assert (
original_regexes[1]["match"] == "4111-1111-1111-1111"
), "Original should be unchanged"
print("Regex matches redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_with_custom_words():
"""Test redaction of custom word matches in word policy"""
response_with_custom_words = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"wordPolicy": {
"customWords": [
{
"match": "confidential_data",
"action": "BLOCKED",
},
{
"match": "secret_information",
"action": "ANONYMIZED",
},
]
}
}
],
"outputs": [{"text": "Custom words detected"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_with_custom_words)
# Verify that custom word matches are redacted
custom_words = redacted_response["assessments"][0]["wordPolicy"]["customWords"]
assert (
custom_words[0]["match"] == "[REDACTED]"
), "First custom word match should be redacted"
assert (
custom_words[1]["match"] == "[REDACTED]"
), "Second custom word match should be redacted"
# Verify other fields are preserved
assert (
custom_words[0]["action"] == "BLOCKED"
), "Custom word action should be preserved"
assert (
custom_words[1]["action"] == "ANONYMIZED"
), "Custom word action should be preserved"
# Verify original response is unchanged
original_custom_words = response_with_custom_words["assessments"][0]["wordPolicy"][
"customWords"
]
assert (
original_custom_words[0]["match"] == "confidential_data"
), "Original should be unchanged"
assert (
original_custom_words[1]["match"] == "secret_information"
), "Original should be unchanged"
print("Custom words redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_with_managed_words():
"""Test redaction of managed word matches in word policy"""
response_with_managed_words = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"wordPolicy": {
"managedWordLists": [
{
"match": "inappropriate_word",
"action": "BLOCKED",
"type": "PROFANITY",
},
{
"match": "offensive_term",
"action": "ANONYMIZED",
"type": "HATE_SPEECH",
},
]
}
}
],
"outputs": [{"text": "Managed words detected"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(response_with_managed_words)
# Verify that managed word matches are redacted
managed_words = redacted_response["assessments"][0]["wordPolicy"][
"managedWordLists"
]
assert (
managed_words[0]["match"] == "[REDACTED]"
), "First managed word match should be redacted"
assert (
managed_words[1]["match"] == "[REDACTED]"
), "Second managed word match should be redacted"
# Verify other fields are preserved
assert (
managed_words[0]["action"] == "BLOCKED"
), "Managed word action should be preserved"
assert (
managed_words[0]["type"] == "PROFANITY"
), "Managed word type should be preserved"
assert (
managed_words[1]["action"] == "ANONYMIZED"
), "Managed word action should be preserved"
assert (
managed_words[1]["type"] == "HATE_SPEECH"
), "Managed word type should be preserved"
# Verify original response is unchanged
original_managed_words = response_with_managed_words["assessments"][0][
"wordPolicy"
]["managedWordLists"]
assert (
original_managed_words[0]["match"] == "inappropriate_word"
), "Original should be unchanged"
assert (
original_managed_words[1]["match"] == "offensive_term"
), "Original should be unchanged"
print("Managed words redaction test passed")
@pytest.mark.asyncio
async def test__redact_pii_matches_comprehensive_coverage():
"""Test redaction across all supported policy types in a single response"""
comprehensive_response = {
"action": "GUARDRAIL_INTERVENED",
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{
"type": "EMAIL",
"match": "user@example.com",
"action": "ANONYMIZED",
}
],
"regexes": [
{
"name": "PHONE_PATTERN",
"match": "555-123-4567",
"action": "BLOCKED",
}
],
},
"wordPolicy": {
"customWords": [
{
"match": "confidential",
"action": "BLOCKED",
}
],
"managedWordLists": [
{
"match": "inappropriate",
"action": "ANONYMIZED",
"type": "PROFANITY",
}
],
},
}
],
"outputs": [{"text": "Multiple policy violations detected"}],
}
# Call the redaction function
redacted_response = _redact_pii_matches(comprehensive_response)
# Verify all match fields are redacted
assessment = redacted_response["assessments"][0]
# PII entities
pii_entities = assessment["sensitiveInformationPolicy"]["piiEntities"]
assert (
pii_entities[0]["match"] == "[REDACTED]"
), "PII entity match should be redacted"
# Regex matches
regexes = assessment["sensitiveInformationPolicy"]["regexes"]
assert regexes[0]["match"] == "[REDACTED]", "Regex match should be redacted"
# Custom words
custom_words = assessment["wordPolicy"]["customWords"]
assert (
custom_words[0]["match"] == "[REDACTED]"
), "Custom word match should be redacted"
# Managed words
managed_words = assessment["wordPolicy"]["managedWordLists"]
assert (
managed_words[0]["match"] == "[REDACTED]"
), "Managed word match should be redacted"
# Verify all other fields are preserved
assert pii_entities[0]["type"] == "EMAIL"
assert regexes[0]["name"] == "PHONE_PATTERN"
assert managed_words[0]["type"] == "PROFANITY"
# Verify original response is unchanged
original_assessment = comprehensive_response["assessments"][0]
assert (
original_assessment["sensitiveInformationPolicy"]["piiEntities"][0]["match"]
== "user@example.com"
)
assert (
original_assessment["sensitiveInformationPolicy"]["regexes"][0]["match"]
== "555-123-4567"
)
assert (
original_assessment["wordPolicy"]["customWords"][0]["match"] == "confidential"
)
assert (
original_assessment["wordPolicy"]["managedWordLists"][0]["match"]
== "inappropriate"
)
print("Comprehensive coverage redaction test passed")