diff --git a/presidio-analyzer/app.py b/presidio-analyzer/app.py index 567a796cd0..751e134b61 100644 --- a/presidio-analyzer/app.py +++ b/presidio-analyzer/app.py @@ -8,13 +8,14 @@ from typing import Tuple from flask import Flask, Response, jsonify, request +from werkzeug.exceptions import HTTPException + from presidio_analyzer import ( AnalyzerEngine, AnalyzerEngineProvider, AnalyzerRequest, BatchAnalyzerEngine, ) -from werkzeug.exceptions import HTTPException DEFAULT_PORT = "3000" DEFAULT_BATCH_SIZE = "500" diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/__init__.py new file mode 100644 index 0000000000..3bb47224d2 --- /dev/null +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/__init__.py @@ -0,0 +1,5 @@ +"""Taiwan specific recognizers.""" + +from .tw_national_id_recognizer import TwNationalIdRecognizer + +__all__ = ["TwNationalIdRecognizer"] diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/tw_national_id_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/tw_national_id_recognizer.py new file mode 100644 index 0000000000..83a977665c --- /dev/null +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/tw/tw_national_id_recognizer.py @@ -0,0 +1,78 @@ +"""Taiwan National ID Recognizer.""" + +from typing import List, Optional + +from presidio_analyzer import Pattern, PatternRecognizer + + +class TwNationalIdRecognizer(PatternRecognizer): + """Recognize Taiwan National ID using patterns and checksums.""" + + PATTERNS = [ + Pattern( + "TW National ID (Strict)", + r"(? bool: + """Reject invalid Taiwan ID structures via Modulus-10 checksum validation.""" + if len(pattern_text) != 10: + return True + + # Presidio uses IGNORECASE by default; explicitly reject lowercase initial letters + first_char = pattern_text[0] + if not first_char.isupper(): + return True + + letter_codes = { + 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, + 'I': 34, 'J': 18, 'K': 19, 'L': 20, 'M': 21, 'N': 22, 'O': 35, 'P': 23, + 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29, 'W': 32, 'X': 30, + 'Y': 31, 'Z': 33 + } + + if first_char not in letter_codes: + return True + + if pattern_text[1] not in ('1', '2', '8', '9'): + return True + + try: + code = letter_codes[first_char] + n1 = code // 10 + n2 = code % 10 + + digits = [n1, n2] + [int(char) for char in pattern_text[1:]] + weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1] + total = sum(d * w for d, w in zip(digits, weights)) + + return total % 10 != 0 + except ValueError: + return True diff --git a/presidio-analyzer/tests/test_tw_national_id_recognizer.py b/presidio-analyzer/tests/test_tw_national_id_recognizer.py new file mode 100644 index 0000000000..99fe8daaf7 --- /dev/null +++ b/presidio-analyzer/tests/test_tw_national_id_recognizer.py @@ -0,0 +1,33 @@ +import pytest +from presidio_analyzer.predefined_recognizers.country_specific.tw import TwNationalIdRecognizer + + +@pytest.fixture(scope="module") +def recognizer(): + return TwNationalIdRecognizer() + + +@pytest.mark.parametrize( + "text, expected_len, expected_positions", + [ + # Valid Taiwan IDs (Verified with Modulus-10 checksum) + ("My ID is A123456789.", 1, ((9, 19),)), + ("B120000008", 1, ((0, 10),)), + ("F120000002", 1, ((0, 10),)), + ("H120000004", 1, ((0, 10),)), + # Adjacent to Chinese characters + ("身分證A123456789", 1, ((3, 13),)), + # Invalid Formats / Non-Matches / Checksum Failures + ("A323456789", 0, ()), # Invalid gender code (3) + ("A12345678", 0, ()), # Too short + ("A1234567890", 0, ()), # Too long + ("1123456789", 0, ()), # Missing letter + ("a123456789", 0, ()), # Lowercase prefix rejected + ("A123456780", 0, ()), # Checksum failure + ], +) +def test_tw_national_id_recognizer(text, expected_len, expected_positions, recognizer): + results = recognizer.analyze(text, entities=["TW_NATIONAL_ID"]) + assert len(results) == expected_len + if expected_len > 0: + assert (results[0].start, results[0].end) == expected_positions[0] diff --git a/presidio-cli/presidio_cli/analyzer.py b/presidio-cli/presidio_cli/analyzer.py index e593203282..5bb11e4c63 100644 --- a/presidio-cli/presidio_cli/analyzer.py +++ b/presidio-cli/presidio_cli/analyzer.py @@ -1,7 +1,6 @@ from typing import Generator, Optional, Union from presidio_analyzer import RecognizerResult - from presidio_cli.config import PresidioCLIConfig diff --git a/presidio-cli/presidio_cli/config.py b/presidio-cli/presidio_cli/config.py index 1e6ac337e1..e073c17021 100644 --- a/presidio-cli/presidio_cli/config.py +++ b/presidio-cli/presidio_cli/config.py @@ -3,6 +3,7 @@ import pathspec import yaml + from presidio_analyzer import AnalyzerEngine diff --git a/presidio-image-redactor/presidio_image_redactor/dicom_image_pii_verify_engine.py b/presidio-image-redactor/presidio_image_redactor/dicom_image_pii_verify_engine.py index 62cd2672d8..541fd7083d 100644 --- a/presidio-image-redactor/presidio_image_redactor/dicom_image_pii_verify_engine.py +++ b/presidio-image-redactor/presidio_image_redactor/dicom_image_pii_verify_engine.py @@ -4,8 +4,8 @@ import PIL import pydicom from PIL import Image -from presidio_analyzer import PatternRecognizer +from presidio_analyzer import PatternRecognizer from presidio_image_redactor import ( OCR, BboxProcessor, diff --git a/presidio-image-redactor/presidio_image_redactor/dicom_image_redactor_engine.py b/presidio-image-redactor/presidio_image_redactor/dicom_image_redactor_engine.py index aa974b8b99..ee06b7d639 100644 --- a/presidio-image-redactor/presidio_image_redactor/dicom_image_redactor_engine.py +++ b/presidio-image-redactor/presidio_image_redactor/dicom_image_redactor_engine.py @@ -9,10 +9,10 @@ import pydicom from matplotlib import pyplot as plt # necessary import for PIL typing # noqa: F401 from PIL import Image, ImageOps -from presidio_analyzer import PatternRecognizer from pydicom.multival import MultiValue from pydicom.pixel_data_handlers.util import apply_voi_lut +from presidio_analyzer import PatternRecognizer from presidio_image_redactor import ( ImageAnalyzerEngine, # noqa: F401 ImageRedactorEngine, diff --git a/presidio-image-redactor/presidio_image_redactor/image_analyzer_engine.py b/presidio-image-redactor/presidio_image_redactor/image_analyzer_engine.py index ad514fc606..afacfe3b2e 100644 --- a/presidio-image-redactor/presidio_image_redactor/image_analyzer_engine.py +++ b/presidio-image-redactor/presidio_image_redactor/image_analyzer_engine.py @@ -6,8 +6,8 @@ import matplotlib.pyplot as plt import numpy as np from PIL import Image, ImageChops -from presidio_analyzer import AnalyzerEngine, RecognizerResult +from presidio_analyzer import AnalyzerEngine, RecognizerResult from presidio_image_redactor import OCR, ImagePreprocessor, TesseractOCR from presidio_image_redactor.entities import ImageRecognizerResult diff --git a/presidio-image-redactor/presidio_image_redactor/image_pii_verify_engine.py b/presidio-image-redactor/presidio_image_redactor/image_pii_verify_engine.py index 83488940d4..704ec1950c 100644 --- a/presidio-image-redactor/presidio_image_redactor/image_pii_verify_engine.py +++ b/presidio-image-redactor/presidio_image_redactor/image_pii_verify_engine.py @@ -2,8 +2,8 @@ from typing import List, Optional from PIL import Image, ImageChops -from presidio_analyzer import PatternRecognizer +from presidio_analyzer import PatternRecognizer from presidio_image_redactor.image_redactor_engine import ImageRedactorEngine diff --git a/presidio-image-redactor/presidio_image_redactor/image_redactor_engine.py b/presidio-image-redactor/presidio_image_redactor/image_redactor_engine.py index bd663a793f..6ac6d72744 100644 --- a/presidio-image-redactor/presidio_image_redactor/image_redactor_engine.py +++ b/presidio-image-redactor/presidio_image_redactor/image_redactor_engine.py @@ -1,9 +1,9 @@ from typing import List, Optional, Tuple, Union -import presidio_analyzer # required for isinstance check which throws an error when trying to specify PatternRecognizer # noqa: E501 from PIL import Image, ImageChops, ImageDraw -from presidio_analyzer import PatternRecognizer +import presidio_analyzer # required for isinstance check which throws an error when trying to specify PatternRecognizer # noqa: E501 +from presidio_analyzer import PatternRecognizer from presidio_image_redactor import BboxProcessor, ImageAnalyzerEngine from presidio_image_redactor.entities import ImageRecognizerResult diff --git a/presidio-structured/presidio_structured/analysis_builder.py b/presidio-structured/presidio_structured/analysis_builder.py index 9fe3d0749f..5438fd42f5 100644 --- a/presidio-structured/presidio_structured/analysis_builder.py +++ b/presidio-structured/presidio_structured/analysis_builder.py @@ -5,13 +5,13 @@ from typing import Dict, Iterator, List, Optional, Union from pandas import DataFrame + from presidio_analyzer import ( AnalyzerEngine, BatchAnalyzerEngine, DictAnalyzerResult, RecognizerResult, ) - from presidio_structured.config import StructuredAnalysis NON_PII_ENTITY_TYPE = "NON_PII"