Add Canadian SIN recognizer (#1934)

This commit is contained in:
Kennion Black
2026-04-09 01:10:16 -06:00
committed by GitHub
parent a3467496a7
commit 060e66928e
8 changed files with 195 additions and 0 deletions

View File

@@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
### Analyzer
#### Added
- Canadian SIN (`CA_SIN`) recognizer for the Canadian Social Insurance Number, using regex pattern matching, context words (English and French), and Luhn checksum validation. Disabled by default.
- Swedish PII recognizers for `SE_PERSONNUMMER` to identify Swedish Personal ID Numbers using pattern match and checksum. The recognizer also supports Swedish coordination numbers (samordningsnummer), issued to individuals who are not registered residents in Sweden but require identification. All disabled by default.
- German PII recognizers for `DE_TAX_ID` (Steueridentifikationsnummer, §§ 139a139e AO, ISO 7064 Mod 11,10 checksum), `DE_TAX_NUMBER` (Steuernummer, § 139a AO, ELSTER and slash formats), `DE_PASSPORT` (Reisepassnummer, PassG § 4, ICAO Doc 9303), `DE_ID_CARD` (Personalausweisnummer, PAuswG), `DE_SOCIAL_SECURITY` (Rentenversicherungsnummer, § 147 SGB VI, DRV checksum), `DE_HEALTH_INSURANCE` (Krankenversicherungsnummer/KVNR, § 290 SGB V, GKV checksum), `DE_KFZ` (KFZ-Kennzeichen, FZV § 8), `DE_HANDELSREGISTER` (Handelsregisternummer HRA/HRB, §§ 9/14 HGB), and `DE_PLZ` (Postleitzahl, very low base confidence, context-only). All disabled by default.

View File

@@ -118,6 +118,12 @@ For more information, refer to the [adding new recognizers documentation](analyz
| NG_NIN | The Nigerian National Identification Number (NIN) is a unique 11-digit number issued by the National Identity Management Commission (NIMC). | Pattern match, context, and checksum |
| NG_VEHICLE_REGISTRATION | Nigerian vehicle registration plate number in the current format (2011+): 3 letters (LGA code), 3 digits (serial), 2 letters (year/batch). | Pattern match and context |
### Canada
|FieldType|Description|Detection Method|
|--- |--- |--- |
|CA_SIN|A Canadian Social Insurance Number (SIN) is a 9-digit number issued by Employment and Social Development Canada (ESDC) to administer government programs. The last digit is a Luhn check digit. SINs starting with 0 or 8 are reserved and not issued.|Pattern match, context, and checksum|
### Sweden
| FieldType | Description | Detection Method |
|------------|---------------------------------------------------------------------------------------------------------|------------------------------------------|

View File

@@ -346,3 +346,10 @@ recognizers:
type: predefined
enabled: false
config_path: presidio_analyzer/conf/langextract_config_basic.yaml
- name: CaSinRecognizer
supported_languages:
- en
- fr
type: predefined
enabled: false

View File

@@ -10,6 +10,9 @@ from .country_specific.australia.au_acn_recognizer import AuAcnRecognizer
from .country_specific.australia.au_medicare_recognizer import AuMedicareRecognizer
from .country_specific.australia.au_tfn_recognizer import AuTfnRecognizer
# Canada recognizers
from .country_specific.canada.ca_sin_recognizer import CaSinRecognizer
# Finland recognizers
from .country_specific.finland.fi_personal_identity_code_recognizer import (
FiPersonalIdentityCodeRecognizer,
@@ -161,6 +164,7 @@ NLP_RECOGNIZERS = {
__all__ = [
"AbaRoutingRecognizer",
"CaSinRecognizer",
"CreditCardRecognizer",
"CryptoRecognizer",
"DateRecognizer",

View File

@@ -1 +1,7 @@
"""Country-specific recognizers package."""
from .canada.ca_sin_recognizer import CaSinRecognizer
__all__ = [
"CaSinRecognizer",
]

View File

@@ -0,0 +1,7 @@
"""Canada-specific recognizers package."""
from .ca_sin_recognizer import CaSinRecognizer
__all__ = [
"CaSinRecognizer",
]

View File

@@ -0,0 +1,84 @@
"""Recognizer for Canadian Social Insurance Number (SIN)."""
from typing import List, Optional
from presidio_analyzer import Pattern, PatternRecognizer
class CaSinRecognizer(PatternRecognizer):
"""Recognize Canadian Social Insurance Number (SIN) using regex + Luhn checksum.
A SIN is a 9-digit number issued by Employment and Social Development Canada
(ESDC) to administer various government programs. The last digit is a Luhn
check digit computed over the first 8 digits. SINs beginning with 0 or 8 are
reserved and not currently issued to individuals.
Format: DDD DDD DDD or DDD-DDD-DDD or DDDDDDDDD
First digit valid range: 1-7, 9
Reference: https://www.canada.ca/en/employment-social-development/services/sin.html
:param patterns: List of patterns to be used by this recognizer
:param context: List of context words to increase confidence in detection
:param supported_language: Language this recognizer supports
:param supported_entity: The entity this recognizer can detect
"""
PATTERNS = [
Pattern("SIN (weak)", r"\b[1-79]\d{8}\b", 0.05),
Pattern("SIN (medium)", r"\b[1-79]\d{2}([- ])\d{3}\1\d{3}\b", 0.5),
]
CONTEXT = [
"sin",
"sin number",
"social insurance",
"social insurance number",
"canada",
# French equivalents
"nas",
"numéro nas",
"numéro d'assurance sociale",
"assurance sociale",
]
def __init__(
self,
patterns: Optional[List[Pattern]] = None,
context: Optional[List[str]] = None,
supported_language: str = "en",
supported_entity: str = "CA_SIN",
name: Optional[str] = None,
):
patterns = patterns if patterns else self.PATTERNS
context = context if context else self.CONTEXT
super().__init__(
supported_entity=supported_entity,
patterns=patterns,
context=context,
supported_language=supported_language,
name=name,
)
def invalidate_result(self, pattern_text: str) -> bool:
"""
Check if the pattern text cannot be validated as a CA_SIN entity.
:param pattern_text: Text detected as pattern by regex
:return: True if invalidated
"""
only_digits = "".join(c for c in pattern_text if c.isdigit())
return not self._luhn_valid(only_digits)
@staticmethod
def _luhn_valid(digits: str) -> bool:
"""Validate using the Luhn checksum."""
total = 0
for i, digit in enumerate(reversed(digits)):
n = int(digit)
if i % 2 == 1:
n *= 2
if n > 9:
n -= 9
total += n
return total % 10 == 0

View File

@@ -0,0 +1,79 @@
import pytest
from tests import assert_result_within_score_range
from presidio_analyzer.predefined_recognizers import CaSinRecognizer
@pytest.fixture(scope="module")
def recognizer():
return CaSinRecognizer()
@pytest.fixture(scope="module")
def entities():
return ["CA_SIN"]
@pytest.mark.parametrize(
"text, expected_len, expected_positions, expected_score_ranges",
[
# fmt: off
# --- Valid SINs ---
# Space delimited
("130 692 544", 1, ((0, 11),), ((0.5, 0.81),),),
("435 418 165", 1, ((0, 11),), ((0.5, 0.81),),),
("948 584 792", 1, ((0, 11),), ((0.5, 0.81),),),
# Hyphen delimited
("347-677-452", 1, ((0, 11),), ((0.5, 0.81),),),
("731-530-150", 1, ((0, 11),), ((0.5, 0.81),),),
# No delimiter
("130692544", 1, ((0, 9),), ((0.0, 0.3),),),
("550090112", 1, ((0, 9),), ((0.0, 0.3),),),
# --- Valid SIN with context ---
("my SIN is 130-692-544", 1, ((10, 21),), ((0.5, 0.81),),),
("mon NAS: 258 933 688", 1, ((9, 20),), ((0.5, 0.81),),),
# --- Invalid: checksum failure ---
("130 692 545", 0, (), (),),
("130692545", 0, (), (),),
("435-418-166", 0, (), (),),
# --- Invalid: reserved first digit ---
("046 454 286", 0, (), (),),
("812 345 678", 0, (), (),),
# --- Invalid: all same digit ---
("111 111 111", 0, (), (),),
("999 999 999", 0, (), (),),
# --- Invalid: mismatched delimiters ---
("046-454 286", 0, (), (),),
("046 454-286", 0, (), (),),
# --- Invalid: wrong length ---
("13069254", 0, (), (),),
("1306925440", 0, (), (),),
# fmt: on
],
)
def test_when_sin_in_text_then_all_ca_sins_are_found(
text,
expected_len,
expected_positions,
expected_score_ranges,
recognizer,
entities,
max_score,
):
results = recognizer.analyze(text, entities)
results = sorted(results, key=lambda x: x.start)
assert len(results) == expected_len
for res, (st_pos, fn_pos), (st_score, fn_score) in zip(
results, expected_positions, expected_score_ranges
):
if fn_score == "max":
fn_score = max_score
assert_result_within_score_range(
res, entities[0], st_pos, fn_pos, st_score, fn_score
)