mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
fix(guardrails): add opt-in mask_pii_fail_closed for Presidio mask-only configs
A Presidio guardrail that only masks PII (no pii_entities_config, output_parse_pii, or apply_to_output) fell through _fail_on_invalid_response to `return []` on an analyzer error, so anonymize_text returned the original text and unmasked PII reached the provider. Add an opt-in `mask_pii_fail_closed` flag (default False, preserving current behaviour) that includes mask-only configs in the fail-closed path, so an analyzer outage blocks instead of forwarding unmasked text. Refs #30728
This commit is contained in:
parent
8e30cfbeb1
commit
00d71caa6c
4 changed files with 67 additions and 2 deletions
|
|
@ -76,6 +76,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
|
|||
presidio_anonymizer_api_base: Optional[str] = None,
|
||||
output_parse_pii: Optional[bool] = False,
|
||||
apply_to_output: bool = False,
|
||||
mask_pii_fail_closed: bool = False,
|
||||
presidio_ad_hoc_recognizers: Optional[str] = None,
|
||||
logging_only: Optional[bool] = None,
|
||||
pii_entities_config: Optional[Dict[Union[PiiEntityType, str], PiiAction]] = None,
|
||||
|
|
@ -93,6 +94,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
|
|||
self.mock_redacted_text = mock_redacted_text
|
||||
self.output_parse_pii = output_parse_pii or False
|
||||
self.apply_to_output = apply_to_output
|
||||
self.mask_pii_fail_closed = mask_pii_fail_closed
|
||||
|
||||
# When output_parse_pii or apply_to_output is enabled, the guardrail must
|
||||
# also run on post_call to unmask/mask the response. Expand the event_hook
|
||||
|
|
@ -298,7 +300,12 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
|
|||
def _fail_on_invalid_response(
|
||||
reason: str,
|
||||
) -> List[PresidioAnalyzeResponseItem]:
|
||||
should_fail_closed = bool(self.pii_entities_config) or self.output_parse_pii or self.apply_to_output
|
||||
should_fail_closed = (
|
||||
bool(self.pii_entities_config)
|
||||
or self.output_parse_pii
|
||||
or self.apply_to_output
|
||||
or self.mask_pii_fail_closed
|
||||
)
|
||||
if should_fail_closed:
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,10 @@ def initialize_presidio(litellm_params: LitellmParams, guardrail: Guardrail):
|
|||
apply_to_output=False,
|
||||
)
|
||||
params.update(overrides)
|
||||
callback = _OPTIONAL_PresidioPIIMasking(**params)
|
||||
callback = _OPTIONAL_PresidioPIIMasking(
|
||||
**params,
|
||||
mask_pii_fail_closed=bool(litellm_params.mask_pii_fail_closed),
|
||||
)
|
||||
litellm.logging_callback_manager.add_litellm_callback(callback)
|
||||
return callback
|
||||
|
||||
|
|
|
|||
|
|
@ -345,6 +345,12 @@ class PresidioPresidioConfigModelUserInterface(BaseModel):
|
|||
# extra param to let the ui know this is a boolean
|
||||
json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL},
|
||||
)
|
||||
mask_pii_fail_closed: Optional[bool] = Field(
|
||||
default=None,
|
||||
description="When True, a mask-only Presidio guardrail blocks the request (fail closed) if the analyzer errors, instead of forwarding unmasked text",
|
||||
# extra param to let the ui know this is a boolean
|
||||
json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL},
|
||||
)
|
||||
presidio_language: Optional[str] = Field(
|
||||
default="en",
|
||||
description="Language code for Presidio PII analysis (e.g., 'en', 'de', 'es', 'fr')",
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,55 @@ async def test_analyze_text_invalid_response_raises_when_mask_configured():
|
|||
assert "PII protection is configured" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_text_mask_only_fails_open_by_default():
|
||||
"""
|
||||
Default mask-only config (no pii_entities_config / output / apply_to_output)
|
||||
still fails open on analyzer error, preserving prior behaviour (issue #30728).
|
||||
"""
|
||||
presidio = _OPTIONAL_PresidioPIIMasking(
|
||||
presidio_analyzer_api_base="http://mock-presidio:5002/",
|
||||
presidio_anonymizer_api_base="http://mock-presidio:5001/",
|
||||
output_parse_pii=False,
|
||||
)
|
||||
assert presidio.mask_pii_fail_closed is False
|
||||
|
||||
with patch.object(
|
||||
presidio,
|
||||
"_get_session_iterator",
|
||||
_make_mock_session_iterator("Internal Server Error", status=500),
|
||||
):
|
||||
result = await presidio.analyze_text(
|
||||
text="some text", presidio_config=None, request_data={}
|
||||
)
|
||||
assert result == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_text_raises_when_mask_pii_fail_closed_set():
|
||||
"""
|
||||
With mask_pii_fail_closed=True, a mask-only guardrail blocks (fail closed)
|
||||
on analyzer error instead of forwarding unmasked text (issue #30728).
|
||||
"""
|
||||
presidio = _OPTIONAL_PresidioPIIMasking(
|
||||
presidio_analyzer_api_base="http://mock-presidio:5002/",
|
||||
presidio_anonymizer_api_base="http://mock-presidio:5001/",
|
||||
output_parse_pii=False,
|
||||
mask_pii_fail_closed=True,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
presidio,
|
||||
"_get_session_iterator",
|
||||
_make_mock_session_iterator("Internal Server Error", status=500),
|
||||
):
|
||||
with pytest.raises(GuardrailRaisedException) as exc_info:
|
||||
await presidio.analyze_text(
|
||||
text="some text", presidio_config=None, request_data={}
|
||||
)
|
||||
assert "PII protection is configured" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_text_list_with_non_dict_items():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue