feat: Add Canadian PII protection (PIPEDA) (#22951)

* feat: Add Canadian PII protection patterns and PIPEDA-compliant policy template

Adds 6 new Canadian PII regex pattern detectors to patterns.json:
- ca_sin: Social Insurance Number (PIPEDA Privacy Act, Income Tax Act)
- ca_ohip: Ontario Health Insurance Plan Number (PHIPA)
- ca_on_drivers_licence: Ontario driver's licence (HTA, PIPEDA)
- ca_immigration_doc: IRCC immigration docs (UCI, work/study permits, IMM refs)
- ca_bank_account: Canadian bank account routing (transit-institution-account)
- ca_postal_code: Canadian postal code (Canada Post spec)

Adds comprehensive policy template 'canadian-pii-protection' (id: canadian-pii-protection)
with 5 sub-guardrails grouping patterns by data type. All patterns include contextual
keyword matching (English + French keywords where applicable) to reduce false positives.
Complements existing passport_canada pattern.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add Canadian PII compliance dataset and tests (57 tests)

Adds:
- test_ca_patterns.py: 30 unit tests for regex pattern matching (SIN, OHIP,
  driver's licence, immigration docs, bank account, postal code)
- test_ca_policy_e2e.py: 27 end-to-end tests running the full
  ContentFilterGuardrail pipeline with MASK action — validates detection
  of real PII and pass-through of clean prompts
- canadianPiiCompliancePrompts.ts: 21-prompt compliance dataset for UI
  evaluation, wired into the main compliancePrompts framework

Fixes keyword_pattern alternation ordering in patterns.json — longer
alternatives (e.g. "social insurance number") now precede shorter ones
("social insurance") to avoid excessive gap-word count when the regex
engine selects the shorter match first.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add University of Toronto FIPPA identifier patterns and tests (36 tests)

Add 3 UofT institutional identifiers (student/employee number, UTORid, TCard)
covered under Ontario FIPPA. Includes pattern definitions, policy template
sub-guardrail, compliance prompts, unit tests, and e2e tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Correct test assertion bug and inaccurate docstring

Fix test_utorid_masked checking `result` (dict) instead of `output` (string).
Update test_ca_policy_e2e.py docstring to clarify scope vs UofT tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Address Greptile review feedback

- Tighten ca_postal_code keyword_pattern: replace broad "address" with
  specific compound terms (mailing/street/shipping/home address)
- Add missing "PIPEDA" tag to policy_templates.json for discoverability
- Add us_phone pattern to test_ca_policy_e2e.py setup to match deployed template
- Add phone number e2e test for complete coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Tighten patterns to reduce false positives and add missing test coverage

- ca_sin: reject leading-zero SINs ([1-9]\d{2}), set allow_word_numbers to false
- ca_immigration_doc: require separators in UCI pattern (prevent bare \d{10} match)
- uoft_utorid: qualify generic keywords (acorn -> acorn login, quercus -> quercus login)
- uoft_tcard: remove generic keywords (student card, id card, library card) that
  overlap with credit card contexts; keep only UofT-specific terms (tcard, campus card)
- Add visa/mastercard/amex/iban patterns to test_ca_policy_e2e.py setup to match
  deployed template; add Visa card masking test
- Add test verifying "student card" no longer triggers TCard redaction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Krish Dholakia 2026-03-06 18:27:31 -08:00 committed by GitHub
parent 7f5d5c5c6e
commit ff8e01d20b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1765 additions and 0 deletions

View file

@ -543,6 +543,87 @@
"description": "Detects Singapore bank account numbers (XXX-XXXXX(X)-X(XX) format for DBS/POSB/OCBC/UOB)",
"keyword_pattern": "\\b(?:bank\\s*account|account\\s*number|DBS|OCBC|UOB|POSB|savings|current\\s*account|bank\\s*statement)\\b",
"allow_word_numbers": false
},
{
"name": "ca_sin",
"display_name": "SIN (Canadian Social Insurance Number)",
"pattern": "\\b[1-9]\\d{2}[\\-\\s]\\d{3}[\\-\\s]\\d{3}\\b",
"category": "Canadian PII Patterns",
"description": "Detects Canadian Social Insurance Numbers (9-digit federal identifier, dashed or spaced format, first digit 1-9)",
"keyword_pattern": "\\b(?:social\\s*insurance\\s*number|numéro\\s*d'assurance\\s*sociale|social\\s*insurance|SIN|NAS)\\b",
"allow_word_numbers": false
},
{
"name": "ca_ohip",
"display_name": "OHIP (Ontario Health Insurance Plan Number)",
"pattern": "\\b\\d{4}[\\-\\s]?\\d{3}[\\-\\s]?\\d{3}[\\-\\s]?[A-Z]{2}\\b",
"category": "Canadian PII Patterns",
"description": "Detects Ontario Health Insurance Plan numbers (10 digits + 2-letter version code)",
"keyword_pattern": "\\b(?:OHIP\\s*number|health\\s*card\\s*number|health\\s*insurance\\s*number|OHIP|health\\s*card|health\\s*insurance|ontario\\s*health|health\\s*number|carte\\s*santé)\\b",
"allow_word_numbers": false
},
{
"name": "ca_on_drivers_licence",
"display_name": "Driver's Licence (Ontario)",
"pattern": "\\b[A-Z]\\d{4}[\\-\\s]\\d{5}[\\-\\s]\\d{5}\\b",
"category": "Canadian PII Patterns",
"description": "Detects Ontario driver's licence numbers (1 letter + 14 digits, dashed format)",
"keyword_pattern": "\\b(?:driver'?s?\\s*licen[cs]e\\s*number|ontario\\s*licen[cs]e\\s*number|driver'?s?\\s*licen[cs]e|licen[cs]e\\s*number|DL\\s*number|ontario\\s*licen[cs]e|permis\\s*de\\s*conduire|numéro\\s*de\\s*permis)\\b",
"allow_word_numbers": false
},
{
"name": "ca_immigration_doc",
"display_name": "Immigration Document (Canada IRCC)",
"pattern": "\\b(?:\\d{4}[\\-\\s]\\d{4}[\\-\\s]\\d{2}|[TUFW]\\d{8,10}|IMM[\\-\\s]?\\d{4,5})\\b",
"category": "Canadian PII Patterns",
"description": "Detects Canadian immigration document numbers including UCI (10-digit), work/study permits, and IMM document references",
"keyword_pattern": "\\b(?:unique\\s*client\\s*identifier|study\\s*permit\\s*number|work\\s*permit\\s*number|study\\s*permit|work\\s*permit|UCI|immigration|IRCC|permanent\\s*resident|PR\\s*card|permis\\s*d'études|permis\\s*de\\s*travail|landed\\s*immigrant|temporary\\s*resident)\\b",
"allow_word_numbers": false
},
{
"name": "ca_bank_account",
"display_name": "Bank Account (Canada)",
"pattern": "\\b\\d{5}[\\-\\s]\\d{3}[\\-\\s]\\d{7,12}\\b",
"category": "Canadian PII Patterns",
"description": "Detects Canadian bank account routing information (transit-institution-account format)",
"keyword_pattern": "\\b(?:bank\\s*account|account\\s*number|transit\\s*number|institution\\s*number|branch\\s*number|routing\\s*number|direct\\s*deposit|void\\s*cheque|chequing|savings\\s*account|numéro\\s*de\\s*compte|dépôt\\s*direct)\\b",
"allow_word_numbers": false
},
{
"name": "ca_postal_code",
"display_name": "Postal Code (Canada)",
"pattern": "\\b[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][\\-\\s]?\\d[ABCEGHJ-NPRSTV-Z]\\d\\b",
"category": "Canadian PII Patterns",
"description": "Detects Canadian postal codes (A1A 1A1 format, excludes invalid first-letter characters per Canada Post spec)",
"keyword_pattern": "\\b(?:postal\\s*code|zip\\s*code|mailing\\s*address|street\\s*address|shipping\\s*address|home\\s*address|code\\s*postal|postcode)\\b",
"allow_word_numbers": false
},
{
"name": "uoft_student_id",
"display_name": "Student/Employee Number (University of Toronto)",
"pattern": "\\b10\\d{8}\\b",
"category": "Canadian Institutional Identifiers (FIPPA)",
"description": "Detects University of Toronto student and employee numbers (10-digit, prefix '10'). Subject to Ontario FIPPA.",
"keyword_pattern": "\\b(?:student\\s*id\\s*number|student\\s*number|employee\\s*id\\s*number|employee\\s*number|student\\s*id|employee\\s*id|utorid|uoft\\s*id|university\\s*of\\s*toronto|u\\s*of\\s*t)\\b",
"allow_word_numbers": false
},
{
"name": "uoft_utorid",
"display_name": "UTORid (University of Toronto Login)",
"pattern": "\\b[a-z]{2,6}\\d{1,4}\\b",
"category": "Canadian Institutional Identifiers (FIPPA)",
"description": "Detects University of Toronto UTORid login identifiers (e.g. smithj12). Context keywords are essential due to the generic format.",
"keyword_pattern": "\\b(?:utorid|utoid|utor\\s*id|uoft\\s*login|university\\s*of\\s*toronto\\s*login|acorn\\s*login|acorn\\s*portal|quercus\\s*login|quercus\\s*course|portal\\s*login|weblogin|shibboleth)\\b",
"allow_word_numbers": false
},
{
"name": "uoft_tcard",
"display_name": "TCard Number (University of Toronto Campus ID)",
"pattern": "\\b\\d{16}\\b",
"category": "Canadian Institutional Identifiers (FIPPA)",
"description": "Detects University of Toronto TCard campus ID card numbers (16-digit). Context keywords are critical to distinguish from payment card numbers.",
"keyword_pattern": "\\b(?:tcard\\s*number|t-card\\s*number|tcard|t-card|campus\\s*card|campus\\s*id|university\\s*card)\\b",
"allow_word_numbers": false
}
]
}

View file

@ -2506,5 +2506,207 @@
"Content Safety"
],
"estimated_latency_ms": 1
},
{
"id": "canadian-pii-protection",
"title": "Canadian PII Protection (PIPEDA)",
"description": "Protects Canadian personal identifiers under PIPEDA, Ontario FIPPA, and provincial privacy legislation. Masks Social Insurance Numbers (SIN), Ontario Health Insurance Plan (OHIP) numbers, Ontario driver's licence numbers, Canadian passport numbers, IRCC immigration documents, Canadian bank account routing information, postal codes, email addresses, phone numbers, and University of Toronto institutional identifiers (student/employee numbers, UTORid, TCard).",
"icon": "ShieldCheckIcon",
"iconColor": "text-red-500",
"iconBg": "bg-red-50",
"guardrails": [
"ca-pii-government-identifiers",
"ca-pii-health-drivers",
"ca-pii-immigration",
"ca-pii-financial",
"ca-pii-contact-information",
"ca-pii-institutional-ids"
],
"complexity": "Medium",
"guardrailDefinitions": [
{
"guardrail_name": "ca-pii-government-identifiers",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "ca_sin",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "passport_canada",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks Canadian Social Insurance Numbers (SIN) and Canadian passport numbers"
}
},
{
"guardrail_name": "ca-pii-health-drivers",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "ca_ohip",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "ca_on_drivers_licence",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks Ontario Health Insurance Plan (OHIP) numbers and Ontario driver's licence numbers"
}
},
{
"guardrail_name": "ca-pii-immigration",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "ca_immigration_doc",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks IRCC immigration document numbers including UCI, work/study permits, and IMM references"
}
},
{
"guardrail_name": "ca-pii-financial",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "ca_bank_account",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "credit_card",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "visa",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "mastercard",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "amex",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "iban",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks Canadian bank account routing information (transit-institution-account) and payment cards"
}
},
{
"guardrail_name": "ca-pii-contact-information",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "email",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "us_phone",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "ca_postal_code",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks email addresses, phone numbers (North American format), and Canadian postal codes"
}
},
{
"guardrail_name": "ca-pii-institutional-ids",
"litellm_params": {
"guardrail": "litellm_content_filter",
"mode": "pre_call",
"patterns": [
{
"pattern_type": "prebuilt",
"pattern_name": "uoft_student_id",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "uoft_utorid",
"action": "MASK"
},
{
"pattern_type": "prebuilt",
"pattern_name": "uoft_tcard",
"action": "MASK"
}
],
"pattern_redaction_format": "[{pattern_name}_REDACTED]"
},
"guardrail_info": {
"description": "Masks University of Toronto institutional identifiers (student/employee numbers, UTORid login, TCard campus ID) under Ontario FIPPA"
}
}
],
"templateData": {
"policy_name": "canadian-pii-protection",
"description": "Canadian PII protection policy under PIPEDA, Ontario FIPPA, and provincial privacy legislation. Masks SIN, OHIP, Ontario driver's licence, passport, IRCC immigration documents, bank account routing info, postal codes, email, phone numbers, and University of Toronto institutional identifiers.",
"guardrails_add": [
"ca-pii-government-identifiers",
"ca-pii-health-drivers",
"ca-pii-immigration",
"ca-pii-financial",
"ca-pii-contact-information",
"ca-pii-institutional-ids"
],
"guardrails_remove": []
},
"tags": [
"PII Protection",
"Canada",
"PIPEDA",
"FIPPA"
],
"estimated_latency_ms": 1
}
]

View file

@ -0,0 +1,209 @@
"""
Test Canadian PII regex patterns added for PIPEDA compliance.
Tests SIN, OHIP, Ontario driver's licence, immigration documents,
bank account, and postal code detection patterns.
"""
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
get_compiled_pattern,
)
class TestCanadianSIN:
"""Test Canadian Social Insurance Number detection"""
def test_dashed_format(self):
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("123-456-789") is not None
def test_spaced_format(self):
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("123 456 789") is not None
def test_sin_in_sentence(self):
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("My SIN is 987-654-321 for tax") is not None
def test_compact_nine_digits_not_matched(self):
"""Compact 9-digit format (no dashes/spaces) should NOT match the dashed pattern"""
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("123456789") is None
def test_too_few_digits_rejected(self):
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("12-345-678") is None
def test_too_many_digits_rejected(self):
pattern = get_compiled_pattern("ca_sin")
assert pattern.search("1234-567-890") is None
class TestCanadianOHIP:
"""Test Ontario Health Insurance Plan Number detection"""
def test_full_format_with_version_code(self):
pattern = get_compiled_pattern("ca_ohip")
assert pattern.search("1234-567-890-AB") is not None
def test_compact_with_version_code(self):
pattern = get_compiled_pattern("ca_ohip")
assert pattern.search("1234567890AB") is not None
def test_spaced_format(self):
pattern = get_compiled_pattern("ca_ohip")
assert pattern.search("1234 567 890 XY") is not None
def test_ohip_in_sentence(self):
pattern = get_compiled_pattern("ca_ohip")
assert (
pattern.search("My OHIP number is 9876543210ZZ for my appointment")
is not None
)
def test_without_version_code_not_matched(self):
"""OHIP pattern requires the 2-letter version code"""
pattern = get_compiled_pattern("ca_ohip")
# 10 digits alone without letters should not match the full OHIP pattern
assert pattern.search("1234567890") is None
def test_lowercase_version_code_detected(self):
"""Compiled with IGNORECASE"""
pattern = get_compiled_pattern("ca_ohip")
assert pattern.search("1234567890ab") is not None
class TestCanadianOntarioDriversLicence:
"""Test Ontario Driver's Licence detection"""
def test_dashed_format(self):
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert pattern.search("A1234-56789-01234") is not None
def test_spaced_format(self):
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert pattern.search("B9876 54321 09876") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert (
pattern.search("Driver's licence C1111-22222-33333 on file") is not None
)
def test_compact_format_not_matched(self):
"""Compact format (no dashes/spaces) uses a separate pattern"""
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert pattern.search("A12345678901234") is None
def test_missing_letter_prefix_rejected(self):
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert pattern.search("11234-56789-01234") is None
def test_wrong_digit_groups_rejected(self):
pattern = get_compiled_pattern("ca_on_drivers_licence")
assert pattern.search("A123-456789-01234") is None
class TestCanadianImmigrationDoc:
"""Test Canadian IRCC Immigration Document detection"""
def test_imm_document_reference(self):
pattern = get_compiled_pattern("ca_immigration_doc")
assert pattern.search("IMM-5257") is not None
assert pattern.search("IMM 1234") is not None
assert pattern.search("IMM5257") is not None
def test_work_study_permit(self):
pattern = get_compiled_pattern("ca_immigration_doc")
assert pattern.search("T123456789") is not None
assert pattern.search("F1234567890") is not None
assert pattern.search("W12345678") is not None
def test_uci_dashed(self):
pattern = get_compiled_pattern("ca_immigration_doc")
assert pattern.search("1234-5678-90") is not None
def test_uci_compact_rejected(self):
"""Compact 10-digit UCI without separators should NOT match (too broad)"""
pattern = get_compiled_pattern("ca_immigration_doc")
assert pattern.search("1234567890") is None
def test_imm_in_sentence(self):
pattern = get_compiled_pattern("ca_immigration_doc")
assert (
pattern.search("Submit immigration form IMM-5645 with your application")
is not None
)
def test_too_short_imm_rejected(self):
pattern = get_compiled_pattern("ca_immigration_doc")
assert pattern.search("IMM-12") is None # Less than 4 digits
class TestCanadianBankAccount:
"""Test Canadian Bank Account routing detection"""
def test_standard_format_dashed(self):
pattern = get_compiled_pattern("ca_bank_account")
assert pattern.search("12345-003-1234567") is not None
def test_spaced_format(self):
pattern = get_compiled_pattern("ca_bank_account")
assert pattern.search("00456 001 9876543210") is not None
def test_longer_account_number(self):
pattern = get_compiled_pattern("ca_bank_account")
assert pattern.search("12345-003-123456789012") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("ca_bank_account")
assert (
pattern.search("Direct deposit to bank account 12345-003-1234567 please")
is not None
)
def test_without_separators_rejected(self):
pattern = get_compiled_pattern("ca_bank_account")
assert pattern.search("123450031234567") is None
class TestCanadianPostalCode:
"""Test Canadian Postal Code detection"""
def test_spaced_format(self):
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("M5V 2T6") is not None
assert pattern.search("K1A 0B1") is not None
assert pattern.search("V6B 3K9") is not None
def test_compact_format(self):
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("M5V2T6") is not None
assert pattern.search("K1A0B1") is not None
def test_dashed_format(self):
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("M5V-2T6") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("Ship to postal code M5V 2T6 in Toronto") is not None
def test_invalid_first_letter_rejected(self):
"""Letters D, F, I, O, Q, U are not valid as first character"""
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("D5V 2T6") is None
assert pattern.search("F1A 0B1") is None
assert pattern.search("I5V 2T6") is None
assert pattern.search("O1A 0B1") is None
assert pattern.search("Q5V 2T6") is None
assert pattern.search("U1A 0B1") is None
def test_lowercase_detected(self):
"""Compiled with IGNORECASE"""
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("m5v 2t6") is not None
def test_all_digits_rejected(self):
pattern = get_compiled_pattern("ca_postal_code")
assert pattern.search("123 456") is None

View file

@ -0,0 +1,518 @@
"""
End-to-end tests for Canadian PII Protection (PIPEDA) policy template.
Tests the federal/provincial PII patterns (SIN, OHIP, driver's licence, passport,
immigration docs, bank account, postal code) validates that PII-containing prompts
are detected/masked and that clean prompts pass through.
University of Toronto institutional identifiers are tested separately in test_uoft_policy_e2e.py.
"""
import pytest
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
ContentFilterGuardrail,
)
from litellm.types.guardrails import ContentFilterAction, ContentFilterPattern
class TestCanadianPIIPolicyE2E:
"""End-to-end tests for Canadian PII policy template"""
def setup_canadian_guardrail(self):
"""
Setup guardrail with all Canadian PII patterns (mimics the policy template)
"""
patterns = [
# Government identifiers
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_sin",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="passport_canada",
action=ContentFilterAction.MASK,
),
# Health & drivers
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_ohip",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_on_drivers_licence",
action=ContentFilterAction.MASK,
),
# Immigration
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_immigration_doc",
action=ContentFilterAction.MASK,
),
# Financial
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_bank_account",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="credit_card",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="visa",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="mastercard",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="amex",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="iban",
action=ContentFilterAction.MASK,
),
# Contact info
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="email",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="us_phone",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="ca_postal_code",
action=ContentFilterAction.MASK,
),
]
return ContentFilterGuardrail(
guardrail_name="canadian-pii-protection",
patterns=patterns,
)
# =====================
# SIN tests
# =====================
@pytest.mark.asyncio
async def test_sin_dashed_masked(self):
"""SIN in dashed format is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My SIN is 123-456-789, please update my tax records."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_SIN_REDACTED]" in output
assert "123-456-789" not in output
@pytest.mark.asyncio
async def test_sin_spaced_masked(self):
"""SIN in spaced format is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "The employee's social insurance number is 987 654 321."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_SIN_REDACTED]" in output
assert "987 654 321" not in output
@pytest.mark.asyncio
async def test_sin_question_passes(self):
"""Question about SIN without actual number passes through"""
guardrail = self.setup_canadian_guardrail()
text = "What is a Social Insurance Number and how do I apply for one?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# OHIP tests
# =====================
@pytest.mark.asyncio
async def test_ohip_dashed_masked(self):
"""OHIP number with version code is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My OHIP number is 1234-567-890-AB, can you verify my coverage?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_OHIP_REDACTED]" in output
assert "1234-567-890-AB" not in output
@pytest.mark.asyncio
async def test_ohip_compact_masked(self):
"""OHIP number in compact format is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "The health card number 9876543210XY needs to be updated in the system."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_OHIP_REDACTED]" in output
assert "9876543210XY" not in output
@pytest.mark.asyncio
async def test_ohip_question_passes(self):
"""Question about OHIP without actual number passes through"""
guardrail = self.setup_canadian_guardrail()
text = "How do I renew my Ontario health card?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Ontario Driver's Licence tests
# =====================
@pytest.mark.asyncio
async def test_drivers_licence_masked(self):
"""Ontario driver's licence is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My driver's licence number is A1234-56789-01234."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_ON_DRIVERS_LICENCE_REDACTED]" in output
assert "A1234-56789-01234" not in output
@pytest.mark.asyncio
async def test_drivers_licence_question_passes(self):
"""Question about driver's licence without actual number passes through"""
guardrail = self.setup_canadian_guardrail()
text = "How do I renew my Ontario driver's licence?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Canadian Passport tests
# =====================
@pytest.mark.asyncio
async def test_passport_masked(self):
"""Canadian passport number is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My Canadian passport number is AB123456."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[PASSPORT_CANADA_REDACTED]" in output
assert "AB123456" not in output
@pytest.mark.asyncio
async def test_passport_question_passes(self):
"""Question about passports without actual number passes through"""
guardrail = self.setup_canadian_guardrail()
text = "How long does it take to renew a Canadian passport?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Immigration Document tests
# =====================
@pytest.mark.asyncio
async def test_imm_form_masked(self):
"""IRCC IMM form reference is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "Please reference immigration form IMM-5257 for the application."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_IMMIGRATION_DOC_REDACTED]" in output
assert "IMM-5257" not in output
@pytest.mark.asyncio
async def test_study_permit_masked(self):
"""IRCC study permit number is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My IRCC study permit number is T123456789."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_IMMIGRATION_DOC_REDACTED]" in output
assert "T123456789" not in output
@pytest.mark.asyncio
async def test_immigration_question_passes(self):
"""Question about immigration without actual numbers passes through"""
guardrail = self.setup_canadian_guardrail()
text = "What documents do I need for a Canadian work permit application?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Bank Account tests
# =====================
@pytest.mark.asyncio
async def test_bank_account_masked(self):
"""Canadian bank account routing info is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My bank account for direct deposit is 12345-003-1234567."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_BANK_ACCOUNT_REDACTED]" in output
assert "12345-003-1234567" not in output
@pytest.mark.asyncio
async def test_visa_card_masked(self):
"""Visa card number is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My Visa card number is 4111111111111111."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" in output
assert "4111111111111111" not in output
@pytest.mark.asyncio
async def test_bank_question_passes(self):
"""Question about banking without actual numbers passes through"""
guardrail = self.setup_canadian_guardrail()
text = "How do I find my bank's transit and institution number?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Phone Number tests
# =====================
@pytest.mark.asyncio
async def test_phone_number_masked(self):
"""North American phone number is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "Call me at (416) 555-1234 to discuss."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" in output
assert "(416) 555-1234" not in output
# =====================
# Postal Code tests
# =====================
@pytest.mark.asyncio
async def test_postal_code_spaced_masked(self):
"""Canadian postal code in spaced format is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "Ship the package to my postal code M5V 2T6."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_POSTAL_CODE_REDACTED]" in output
assert "M5V 2T6" not in output
@pytest.mark.asyncio
async def test_postal_code_compact_masked(self):
"""Canadian postal code in compact format is detected and masked"""
guardrail = self.setup_canadian_guardrail()
text = "My mailing address postal code is K1A0B1."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[CA_POSTAL_CODE_REDACTED]" in output
assert "K1A0B1" not in output
@pytest.mark.asyncio
async def test_postal_code_question_passes(self):
"""Question about postal codes without actual code passes through"""
guardrail = self.setup_canadian_guardrail()
text = "What is the format of a Canadian postal code?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
# =====================
# Combined / edge case tests
# =====================
@pytest.mark.asyncio
async def test_normal_text_passes(self):
"""Normal text without any PII passes through unchanged"""
guardrail = self.setup_canadian_guardrail()
text = "Please schedule a meeting for next Tuesday to discuss the project."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "REDACTED" not in output
assert output == text
@pytest.mark.asyncio
async def test_multiple_pii_types_masked(self):
"""Multiple Canadian PII types in same message are all masked"""
guardrail = self.setup_canadian_guardrail()
text = (
"Employee SIN 123-456-789, "
"email jane@example.com, "
"postal code M5V 2T6."
)
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "123-456-789" not in output
assert "jane@example.com" not in output
assert "M5V 2T6" not in output
assert "CA_SIN_REDACTED" in output
assert "EMAIL_REDACTED" in output
assert "CA_POSTAL_CODE_REDACTED" in output
@pytest.mark.asyncio
async def test_invalid_postal_code_first_letter_passes(self):
"""Postal code with invalid first letter (D, F, I, O, Q, U) is not masked"""
guardrail = self.setup_canadian_guardrail()
text = "The code D5V 2T6 is not a valid postal code."
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "D5V 2T6" in output
assert "POSTAL_CODE" not in output

View file

@ -0,0 +1,115 @@
"""
Test University of Toronto identifier regex patterns added for FIPPA compliance.
Tests UTORid, student/employee number, and TCard number detection patterns.
"""
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
get_compiled_pattern,
)
class TestUofTStudentId:
"""Test University of Toronto Student/Employee Number detection"""
def test_standard_student_number(self):
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("1012345678") is not None
def test_another_valid_number(self):
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("1099999999") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("uoft_student_id")
assert (
pattern.search("My student number is 1012345678 for registration")
is not None
)
def test_prefix_must_be_10(self):
"""All UofT numbers start with 10"""
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("2012345678") is None
def test_too_few_digits_rejected(self):
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("101234567") is None # 9 digits
def test_too_many_digits_rejected(self):
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("10123456789") is None # 11 digits
def test_non_numeric_rejected(self):
pattern = get_compiled_pattern("uoft_student_id")
assert pattern.search("10abcdefgh") is None
class TestUofTUTORid:
"""Test University of Toronto UTORid detection"""
def test_standard_utorid(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("smithj12") is not None
def test_short_name_prefix(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("li5") is not None
def test_longer_name_prefix(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("kcheng42") is not None
def test_max_length_prefix(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("abcdef1234") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("My UTORid is smithj12 for ACORN login") is not None
def test_single_letter_prefix_rejected(self):
"""Minimum 2 letters required"""
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("a1") is None
def test_too_many_letters_rejected(self):
"""Maximum 6 letters"""
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("abcdefg1") is None
def test_no_digits_rejected(self):
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("smith") is None
def test_too_many_digits_rejected(self):
"""Maximum 4 digits"""
pattern = get_compiled_pattern("uoft_utorid")
assert pattern.search("ab12345") is None
class TestUofTTCard:
"""Test University of Toronto TCard number detection"""
def test_standard_tcard(self):
pattern = get_compiled_pattern("uoft_tcard")
assert pattern.search("1234567890123456") is not None
def test_in_sentence(self):
pattern = get_compiled_pattern("uoft_tcard")
assert (
pattern.search("My TCard number is 1234567890123456 for library access")
is not None
)
def test_too_few_digits_rejected(self):
pattern = get_compiled_pattern("uoft_tcard")
assert pattern.search("123456789012345") is None # 15 digits
def test_too_many_digits_rejected(self):
pattern = get_compiled_pattern("uoft_tcard")
assert pattern.search("12345678901234567") is None # 17 digits
def test_non_numeric_rejected(self):
pattern = get_compiled_pattern("uoft_tcard")
assert pattern.search("123456789012345a") is None

View file

@ -0,0 +1,266 @@
"""
End-to-end tests for University of Toronto identifier patterns (FIPPA compliance).
Tests the complete guardrail with UofT patterns validates that
institutional identifiers are detected/masked and that clean prompts pass through.
"""
import pytest
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
ContentFilterGuardrail,
)
from litellm.types.guardrails import ContentFilterAction, ContentFilterPattern
class TestUofTPolicyE2E:
"""End-to-end tests for University of Toronto identifier patterns"""
def setup_uoft_guardrail(self):
"""
Setup guardrail with all UofT patterns (mimics the policy template sub-guardrail)
"""
patterns = [
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="uoft_student_id",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="uoft_utorid",
action=ContentFilterAction.MASK,
),
ContentFilterPattern(
pattern_type="prebuilt",
pattern_name="uoft_tcard",
action=ContentFilterAction.MASK,
),
]
guardrail = ContentFilterGuardrail(
guardrail_name="test-uoft-ids",
patterns=patterns,
pattern_redaction_format="[{pattern_name}_REDACTED]",
)
return guardrail
# =====================
# Student/Employee Number tests
# =====================
@pytest.mark.asyncio
async def test_student_number_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "My student number is 1012345678 for registration"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_STUDENT_ID_REDACTED]" in output
assert "1012345678" not in output
@pytest.mark.asyncio
async def test_employee_id_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "Employee id 1099887766 needs access to the lab"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_STUDENT_ID_REDACTED]" in output
assert "1099887766" not in output
@pytest.mark.asyncio
async def test_uoft_student_id_in_context(self):
guardrail = self.setup_uoft_guardrail()
text = "University of Toronto student 1012345678 enrolled in CS"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_STUDENT_ID_REDACTED]" in output
assert "1012345678" not in output
@pytest.mark.asyncio
async def test_student_number_clean_passes(self):
guardrail = self.setup_uoft_guardrail()
text = "How do I find my U of T student number?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text
# =====================
# UTORid tests
# =====================
@pytest.mark.asyncio
async def test_utorid_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "My UTORid is smithj12 for ACORN login"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_UTORID_REDACTED]" in output
assert "smithj12" not in output
@pytest.mark.asyncio
async def test_utorid_quercus_context_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "Quercus login kcheng42"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_UTORID_REDACTED]" in output
assert "kcheng42" not in output
@pytest.mark.asyncio
async def test_utorid_acorn_context_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "ACORN login li5"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_UTORID_REDACTED]" in output
assert "li5" not in output
@pytest.mark.asyncio
async def test_utorid_clean_passes(self):
guardrail = self.setup_uoft_guardrail()
text = "How do I reset my UTORid password?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text
# =====================
# TCard tests
# =====================
@pytest.mark.asyncio
async def test_tcard_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "My TCard number is 1234567890123456 for library access"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_TCARD_REDACTED]" in output
assert "1234567890123456" not in output
@pytest.mark.asyncio
async def test_campus_card_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "Campus card 9876543210987654 needs reactivation"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_TCARD_REDACTED]" in output
assert "9876543210987654" not in output
@pytest.mark.asyncio
async def test_university_card_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "Lost my university card 1111222233334444 yesterday"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "[UOFT_TCARD_REDACTED]" in output
assert "1111222233334444" not in output
@pytest.mark.asyncio
async def test_student_card_no_longer_triggers_tcard(self):
"""Generic 'student card' keyword should NOT trigger TCard (avoids credit card collision)"""
guardrail = self.setup_uoft_guardrail()
text = "My student card number is 4111111111111111"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert "UOFT_TCARD_REDACTED" not in output
@pytest.mark.asyncio
async def test_tcard_clean_passes(self):
guardrail = self.setup_uoft_guardrail()
text = "Where can I get a replacement TCard on campus?"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text
# =====================
# No false positives without context
# =====================
@pytest.mark.asyncio
async def test_generic_10digit_no_context_passes(self):
"""10-digit number starting with 10 but no keyword context should pass"""
guardrail = self.setup_uoft_guardrail()
text = "The order total is 1012345678 units"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text
@pytest.mark.asyncio
async def test_generic_short_word_no_context_passes(self):
"""Generic short word with digits should not be masked without UTORid context"""
guardrail = self.setup_uoft_guardrail()
text = "The variable abc123 is used in the code"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text
@pytest.mark.asyncio
async def test_generic_16digit_no_context_passes(self):
"""16-digit number without TCard context should not match TCard pattern"""
guardrail = self.setup_uoft_guardrail()
text = "Reference number 1234567890123456 for the shipment"
result = await guardrail.apply_guardrail(
inputs={"texts": [text]},
request_data={},
input_type="request",
)
output = result.get("texts", [])[0]
assert output == text

View file

@ -0,0 +1,362 @@
import type { CompliancePrompt } from "./compliancePrompts";
export const canadianPiiCompliancePrompts: CompliancePrompt[] = [
// =====================
// SIN — Social Insurance Number (should FAIL = detected/masked)
// =====================
{
id: "ca-sin-1",
framework: "Canadian PII (PIPEDA)",
category: "Social Insurance Number (SIN)",
categoryIcon: "shield",
categoryDescription:
"Detects Canadian Social Insurance Numbers — 9-digit federal identifiers protected under the Privacy Act and PIPEDA.",
prompt:
"My SIN is 123-456-789, please update my tax records.",
expectedResult: "fail",
},
{
id: "ca-sin-2",
framework: "Canadian PII (PIPEDA)",
category: "Social Insurance Number (SIN)",
categoryIcon: "shield",
categoryDescription:
"Detects Canadian Social Insurance Numbers — 9-digit federal identifiers protected under the Privacy Act and PIPEDA.",
prompt:
"The employee's social insurance number is 987 654 321.",
expectedResult: "fail",
},
{
id: "ca-sin-3",
framework: "Canadian PII (PIPEDA)",
category: "Social Insurance Number (SIN)",
categoryIcon: "shield",
categoryDescription:
"Detects Canadian Social Insurance Numbers — 9-digit federal identifiers protected under the Privacy Act and PIPEDA.",
prompt:
"What is a Social Insurance Number and how do I apply for one?",
expectedResult: "pass",
},
// =====================
// OHIP — Ontario Health Insurance Plan (should FAIL = detected/masked)
// =====================
{
id: "ca-ohip-1",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Health Insurance (OHIP)",
categoryIcon: "heart",
categoryDescription:
"Detects Ontario Health Insurance Plan numbers — protected under PHIPA (Personal Health Information Protection Act).",
prompt:
"My OHIP number is 1234-567-890-AB, can you verify my coverage?",
expectedResult: "fail",
},
{
id: "ca-ohip-2",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Health Insurance (OHIP)",
categoryIcon: "heart",
categoryDescription:
"Detects Ontario Health Insurance Plan numbers — protected under PHIPA (Personal Health Information Protection Act).",
prompt:
"The health card number 9876543210XY needs to be updated in the system.",
expectedResult: "fail",
},
{
id: "ca-ohip-3",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Health Insurance (OHIP)",
categoryIcon: "heart",
categoryDescription:
"Detects Ontario Health Insurance Plan numbers — protected under PHIPA (Personal Health Information Protection Act).",
prompt:
"How do I renew my Ontario health card?",
expectedResult: "pass",
},
// =====================
// Ontario Driver's Licence (should FAIL = detected/masked)
// =====================
{
id: "ca-dl-1",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Driver's Licence",
categoryIcon: "credit-card",
categoryDescription:
"Detects Ontario driver's licence numbers — protected under the Highway Traffic Act and PIPEDA.",
prompt:
"My driver's licence number is A1234-56789-01234.",
expectedResult: "fail",
},
{
id: "ca-dl-2",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Driver's Licence",
categoryIcon: "credit-card",
categoryDescription:
"Detects Ontario driver's licence numbers — protected under the Highway Traffic Act and PIPEDA.",
prompt:
"Please update licence number B9876-54321-09876 in the file.",
expectedResult: "fail",
},
{
id: "ca-dl-3",
framework: "Canadian PII (PIPEDA)",
category: "Ontario Driver's Licence",
categoryIcon: "credit-card",
categoryDescription:
"Detects Ontario driver's licence numbers — protected under the Highway Traffic Act and PIPEDA.",
prompt:
"How do I renew my Ontario driver's licence?",
expectedResult: "pass",
},
// =====================
// Canadian Passport (should FAIL = detected/masked)
// =====================
{
id: "ca-passport-1",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Passport",
categoryIcon: "globe",
categoryDescription:
"Detects Canadian passport numbers — protected under the Canadian Passport Order and PIPEDA.",
prompt:
"My Canadian passport number is AB123456.",
expectedResult: "fail",
},
{
id: "ca-passport-2",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Passport",
categoryIcon: "globe",
categoryDescription:
"Detects Canadian passport numbers — protected under the Canadian Passport Order and PIPEDA.",
prompt:
"How long does it take to renew a Canadian passport?",
expectedResult: "pass",
},
// =====================
// Immigration Documents (should FAIL = detected/masked)
// =====================
{
id: "ca-imm-1",
framework: "Canadian PII (PIPEDA)",
category: "IRCC Immigration Documents",
categoryIcon: "file-text",
categoryDescription:
"Detects Canadian immigration document numbers (UCI, work/study permits, IMM forms) — protected under IRPA and PIPEDA.",
prompt:
"My IRCC study permit number is T123456789.",
expectedResult: "fail",
},
{
id: "ca-imm-2",
framework: "Canadian PII (PIPEDA)",
category: "IRCC Immigration Documents",
categoryIcon: "file-text",
categoryDescription:
"Detects Canadian immigration document numbers (UCI, work/study permits, IMM forms) — protected under IRPA and PIPEDA.",
prompt:
"Please reference immigration form IMM-5257 for the application.",
expectedResult: "fail",
},
{
id: "ca-imm-3",
framework: "Canadian PII (PIPEDA)",
category: "IRCC Immigration Documents",
categoryIcon: "file-text",
categoryDescription:
"Detects Canadian immigration document numbers (UCI, work/study permits, IMM forms) — protected under IRPA and PIPEDA.",
prompt:
"What documents do I need for a Canadian work permit application?",
expectedResult: "pass",
},
// =====================
// Bank Account (should FAIL = detected/masked)
// =====================
{
id: "ca-bank-1",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Bank Account",
categoryIcon: "dollar-sign",
categoryDescription:
"Detects Canadian bank account routing information (transit-institution-account format) — protected under the Bank Act and PIPEDA.",
prompt:
"My bank account for direct deposit is 12345-003-1234567.",
expectedResult: "fail",
},
{
id: "ca-bank-2",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Bank Account",
categoryIcon: "dollar-sign",
categoryDescription:
"Detects Canadian bank account routing information (transit-institution-account format) — protected under the Bank Act and PIPEDA.",
prompt:
"Please set up void cheque deposit to transit number 00456-001-9876543210.",
expectedResult: "fail",
},
{
id: "ca-bank-3",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Bank Account",
categoryIcon: "dollar-sign",
categoryDescription:
"Detects Canadian bank account routing information (transit-institution-account format) — protected under the Bank Act and PIPEDA.",
prompt:
"How do I find my bank's transit and institution number?",
expectedResult: "pass",
},
// =====================
// Postal Code (should FAIL = detected/masked)
// =====================
{
id: "ca-postal-1",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Postal Code",
categoryIcon: "map-pin",
categoryDescription:
"Detects Canadian postal codes (A1A 1A1 format) — considered PII when combined with other identifiers under PIPEDA.",
prompt:
"Ship the package to my postal code M5V 2T6.",
expectedResult: "fail",
},
{
id: "ca-postal-2",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Postal Code",
categoryIcon: "map-pin",
categoryDescription:
"Detects Canadian postal codes (A1A 1A1 format) — considered PII when combined with other identifiers under PIPEDA.",
prompt:
"My mailing address postal code is K1A0B1.",
expectedResult: "fail",
},
{
id: "ca-postal-3",
framework: "Canadian PII (PIPEDA)",
category: "Canadian Postal Code",
categoryIcon: "map-pin",
categoryDescription:
"Detects Canadian postal codes (A1A 1A1 format) — considered PII when combined with other identifiers under PIPEDA.",
prompt:
"What is the format of a Canadian postal code?",
expectedResult: "pass",
},
// =====================
// UofT Student/Employee Number (should FAIL = detected/masked)
// =====================
{
id: "ca-uoft-id-1",
framework: "Canadian PII (FIPPA)",
category: "UofT Student/Employee Number",
categoryIcon: "graduation-cap",
categoryDescription:
"Detects University of Toronto student and employee numbers (10-digit, prefix '10') — protected under Ontario FIPPA.",
prompt:
"My student number is 1012345678 for course registration.",
expectedResult: "fail",
},
{
id: "ca-uoft-id-2",
framework: "Canadian PII (FIPPA)",
category: "UofT Student/Employee Number",
categoryIcon: "graduation-cap",
categoryDescription:
"Detects University of Toronto student and employee numbers (10-digit, prefix '10') — protected under Ontario FIPPA.",
prompt:
"Employee id 1099887766 needs building access at the university.",
expectedResult: "fail",
},
{
id: "ca-uoft-id-3",
framework: "Canadian PII (FIPPA)",
category: "UofT Student/Employee Number",
categoryIcon: "graduation-cap",
categoryDescription:
"Detects University of Toronto student and employee numbers (10-digit, prefix '10') — protected under Ontario FIPPA.",
prompt:
"How do I find my U of T student number?",
expectedResult: "pass",
},
// =====================
// UTORid (should FAIL = detected/masked)
// =====================
{
id: "ca-utorid-1",
framework: "Canadian PII (FIPPA)",
category: "UTORid Login",
categoryIcon: "log-in",
categoryDescription:
"Detects University of Toronto UTORid login identifiers — protected under Ontario FIPPA.",
prompt:
"My UTORid is smithj12.",
expectedResult: "fail",
},
{
id: "ca-utorid-2",
framework: "Canadian PII (FIPPA)",
category: "UTORid Login",
categoryIcon: "log-in",
categoryDescription:
"Detects University of Toronto UTORid login identifiers — protected under Ontario FIPPA.",
prompt:
"Quercus login kcheng42 needs password reset.",
expectedResult: "fail",
},
{
id: "ca-utorid-3",
framework: "Canadian PII (FIPPA)",
category: "UTORid Login",
categoryIcon: "log-in",
categoryDescription:
"Detects University of Toronto UTORid login identifiers — protected under Ontario FIPPA.",
prompt:
"How do I reset my UTORid password?",
expectedResult: "pass",
},
// =====================
// TCard (should FAIL = detected/masked)
// =====================
{
id: "ca-tcard-1",
framework: "Canadian PII (FIPPA)",
category: "TCard Campus ID",
categoryIcon: "credit-card",
categoryDescription:
"Detects University of Toronto TCard campus ID card numbers (16-digit) — protected under Ontario FIPPA.",
prompt:
"My TCard number is 1234567890123456 for library access.",
expectedResult: "fail",
},
{
id: "ca-tcard-2",
framework: "Canadian PII (FIPPA)",
category: "TCard Campus ID",
categoryIcon: "credit-card",
categoryDescription:
"Detects University of Toronto TCard campus ID card numbers (16-digit) — protected under Ontario FIPPA.",
prompt:
"Campus card 9876543210987654 needs reactivation.",
expectedResult: "fail",
},
{
id: "ca-tcard-3",
framework: "Canadian PII (FIPPA)",
category: "TCard Campus ID",
categoryIcon: "credit-card",
categoryDescription:
"Detects University of Toronto TCard campus ID card numbers (16-digit) — protected under Ontario FIPPA.",
prompt:
"Where can I get a replacement TCard on campus?",
expectedResult: "pass",
},
];

View file

@ -1,6 +1,7 @@
import { insultsCompliancePrompts } from "./insultsCompliancePrompts";
import { financialCompliancePrompts } from "./financialCompliancePrompts";
import { codeExecutionCompliancePrompts } from "./codeExecutionCompliancePrompts";
import { canadianPiiCompliancePrompts } from "./canadianPiiCompliancePrompts";
import { claimsCompliancePrompts } from "./claimsCompliancePrompts";
export interface CompliancePrompt {
@ -258,6 +259,7 @@ const compliancePrompts: CompliancePrompt[] = [
...insultsCompliancePrompts,
...financialCompliancePrompts,
...codeExecutionCompliancePrompts,
...canadianPiiCompliancePrompts,
...claimsCompliancePrompts,
];
@ -536,6 +538,16 @@ const frameworkMeta: Record<string, { icon: string; description: string }> = {
icon: "shield",
description: "Content filter guardrails that block messages matching specific prohibited topics while allowing legitimate use of related words in context.",
},
"Canadian PII (PIPEDA)": {
icon: "shield",
description:
"Canadian PII detection under PIPEDA and provincial privacy legislation — masks SIN, OHIP, driver's licence, passport, immigration docs, bank accounts, and postal codes.",
},
"Canadian PII (FIPPA)": {
icon: "graduation-cap",
description:
"Ontario FIPPA institutional identifier detection — masks University of Toronto student/employee numbers, UTORid logins, and TCard campus IDs.",
},
"Airline Brand Protection": {
icon: "plane",
description: "Destination vs competitor intent — avoid answering competitor comparison questions.",