mirror of
https://github.com/data-privacy-stack/presidio.git
synced 2026-07-23 11:20:55 -05:00
Add Canadian SIN recognizer (#1934)
This commit is contained in:
@@ -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, §§ 139a–139e 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.
|
||||
|
||||
@@ -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 |
|
||||
|------------|---------------------------------------------------------------------------------------------------------|------------------------------------------|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
"""Country-specific recognizers package."""
|
||||
|
||||
from .canada.ca_sin_recognizer import CaSinRecognizer
|
||||
|
||||
__all__ = [
|
||||
"CaSinRecognizer",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Canada-specific recognizers package."""
|
||||
|
||||
from .ca_sin_recognizer import CaSinRecognizer
|
||||
|
||||
__all__ = [
|
||||
"CaSinRecognizer",
|
||||
]
|
||||
@@ -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
|
||||
79
presidio-analyzer/tests/test_ca_sin_recognizer.py
Normal file
79
presidio-analyzer/tests/test_ca_sin_recognizer.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user