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>
This commit is contained in:
Krrish Dholakia 2026-03-05 19:45:52 -08:00
parent f55fda3be7
commit d35a95fc5d
6 changed files with 547 additions and 5 deletions

View file

@ -597,6 +597,33 @@
"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|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|acorn|quercus|uoft\\s*login|university\\s*of\\s*toronto\\s*login|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|campus\\s*card\\s*number|campus\\s*id\\s*number|library\\s*card\\s*number|student\\s*card\\s*number|id\\s*card\\s*number|tcard|t-card|campus\\s*card|campus\\s*id|library\\s*card|student\\s*card|id\\s*card|university\\s*card)\\b",
"allow_word_numbers": false
}
]
}

View file

@ -2510,7 +2510,7 @@
{
"id": "canadian-pii-protection",
"title": "Canadian PII Protection (PIPEDA)",
"description": "Protects Canadian personal identifiers under PIPEDA 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, and phone numbers.",
"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",
@ -2519,7 +2519,8 @@
"ca-pii-health-drivers",
"ca-pii-immigration",
"ca-pii-financial",
"ca-pii-contact-information"
"ca-pii-contact-information",
"ca-pii-institutional-ids"
],
"complexity": "Medium",
"guardrailDefinitions": [
@ -2657,23 +2658,53 @@
"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 and provincial privacy legislation. Masks SIN, OHIP, Ontario driver's licence, passport, IRCC immigration documents, bank account routing info, postal codes, email, and phone numbers.",
"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-contact-information",
"ca-pii-institutional-ids"
],
"guardrails_remove": []
},
"tags": [
"PII Protection",
"Canada"
"Canada",
"FIPPA"
],
"estimated_latency_ms": 1
}

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,253 @@
"""
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 result
@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 = "Log in to ACORN with 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_student_card_masked(self):
guardrail = self.setup_uoft_guardrail()
text = "Lost my student 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_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

@ -248,4 +248,115 @@ export const canadianPiiCompliancePrompts: CompliancePrompt[] = [
"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 for ACORN login.",
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

@ -543,6 +543,11 @@ const frameworkMeta: Record<string, { icon: string; description: string }> = {
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.",