From 5032e9297a750143004d24ca21a96dc11618aeac Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Mon, 20 Jul 2026 17:48:13 -0500 Subject: [PATCH 1/9] feat: Add UuidRecognizer for detecting UUIDs (v1-v5, v7) --- CHANGELOG.md | 3 + .../conf/default_recognizers.yaml | 3 + .../predefined_recognizers/__init__.py | 2 + .../generic/uuid_recognizer.py | 84 ++++++++++++++++ .../tests/test_recognizer_registry.py | 4 +- .../tests/test_uuid_recognizer.py | 95 +++++++++++++++++++ 6 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py create mode 100644 presidio-analyzer/tests/test_uuid_recognizer.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 40890a66eb..79a2383fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Analyzer +#### Added +- Added `UuidRecognizer` (generic, entity type `UUID`) to detect UUIDs in the standard 8-4-4-4-12 hyphenated hexadecimal format, covering RFC 4122 versions 1-5 and RFC 9562 version 7 (timestamp-based). Validates version and variant nibbles and filters the nil UUID to reduce false positives. + #### Fixed - Language model recognizers (`BasicLangExtractRecognizer`, `AzureOpenAILangExtractRecognizer`) configured in a recognizer registry YAML now honour `config_path` (and other recognizer-specific kwargs). Previously these entries were validated by the strict `PredefinedRecognizerConfig` schema, which has no `config_path` field and does not allow extra keys, so `config_path` was silently dropped and the recognizer fell back to its bundled default model configuration. Added a `LangExtractRecognizerConfig` model (`extra="allow"`) and registered both recognizer class names in `CONFIG_MODEL_MAP`. - `BasicLangExtractRecognizer` now honours values under `langextract.model.provider.language_model_params` (including `timeout` and `num_ctx`). Previously these were silently dropped because `langextract.extract()` ignores its `language_model_params` argument when a pre-built `ModelConfig` is passed via `config=`, causing Ollama-backed recognizers to fall back to langextract's 120s default regardless of the configured timeout. The recognizer now merges `language_model_params` into `ModelConfig.provider_kwargs`, which is the path that reaches the provider constructor. Explicit entries under `provider.kwargs:` still take precedence. Also fixed a `TypeError` when `kwargs:` or `language_model_params:` is `null` in the YAML. (#1943, Thanks @lsternlicht) diff --git a/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml b/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml index a82c62ddaa..b4b28a9728 100644 --- a/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml +++ b/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml @@ -381,6 +381,9 @@ recognizers: - name: MacAddressRecognizer type: predefined + - name: UuidRecognizer + type: predefined + - name: PhoneRecognizer type: predefined diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py index 3715da7c30..9707087275 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py @@ -146,6 +146,7 @@ from .generic.mac_recognizer import MacAddressRecognizer from .generic.phone_recognizer import PhoneRecognizer from .generic.url_recognizer import UrlRecognizer +from .generic.uuid_recognizer import UuidRecognizer # NER recognizers from .ner.gliner_recognizer import GLiNERRecognizer @@ -195,6 +196,7 @@ "NhsRecognizer", "MedicalLicenseRecognizer", "MacAddressRecognizer", + "UuidRecognizer", "PhoneRecognizer", "SgFinRecognizer", "UrlRecognizer", diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py new file mode 100644 index 0000000000..682e06e4a9 --- /dev/null +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py @@ -0,0 +1,84 @@ +import re +from typing import List, Optional + +from presidio_analyzer import Pattern, PatternRecognizer + + +class UuidRecognizer(PatternRecognizer): + """ + Recognize UUID (Universally Unique Identifier) using regex. + + Supports the standard 8-4-4-4-12 hyphenated hexadecimal format for + UUID versions 1-5 (RFC 4122) and version 7 (RFC 9562, timestamp-based, + increasingly common in modern distributed systems, databases, and logs). + + ref: + - https://datatracker.ietf.org/doc/html/rfc4122 + - https://datatracker.ietf.org/doc/html/rfc9562 + """ + + PATTERNS = [ + Pattern( + "UUID (hyphenated)", + r"\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\b", + 0.5, + ), + ] + + CONTEXT = ["uuid", "guid", "unique identifier"] + + # Valid version nibble values: 1-5 (RFC 4122) and 7 (RFC 9562) + VALID_VERSIONS = {"1", "2", "3", "4", "5", "7"} + + # Valid variant bits (RFC 4122 variant): first hex digit of the + # 4th group must be 8, 9, a, or b + VALID_VARIANT_PREFIXES = {"8", "9", "a", "b"} + + NIL_UUID = "00000000-0000-0000-0000-000000000000" + + def __init__( + self, + patterns: Optional[List[Pattern]] = None, + context: Optional[List[str]] = None, + supported_language: str = "en", + supported_entity: str = "UUID", + 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 is a valid UUID format. + + Validates the RFC 4122/9562 version nibble and variant bits to + reduce false positives from arbitrary hex-hyphen strings, and + filters out the nil UUID (all zeros), which is a well-known + sentinel value rather than an identifying value. + + :param pattern_text: Text detected as pattern by regex + :return: True if invalidated (invalid or non-identifying UUID) + """ + if pattern_text.lower() == self.NIL_UUID: + return True + + groups = pattern_text.split("-") + if len(groups) != 5: + return True + + version_nibble = groups[2][0].lower() + if version_nibble not in self.VALID_VERSIONS: + return True + + variant_nibble = groups[3][0].lower() + if variant_nibble not in self.VALID_VARIANT_PREFIXES: + return True + + return False diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index a7b2d596b3..cac0f87ad1 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -54,8 +54,8 @@ def test_when_get_recognizers_then_all_recognizers_returned(mock_recognizer_regi registry.load_predefined_recognizers() recognizers = registry.get_recognizers(language="en", all_fields=True) - # 1 custom recognizer in english + 28 predefined - 11 disabled - assert len(recognizers) == 1 + 28 - 11 + # 1 custom recognizer in english + 29 predefined - 11 disabled + assert len(recognizers) == 1 + 29 - 11 def test_when_get_recognizers_then_return_all_fields(mock_recognizer_registry): diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py new file mode 100644 index 0000000000..cf16ac12a2 --- /dev/null +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -0,0 +1,95 @@ +import pytest +from presidio_analyzer.predefined_recognizers import UuidRecognizer + +from tests import assert_result_within_score_range + + +@pytest.fixture(scope="module") +def recognizer(): + """Return a UuidRecognizer instance for testing.""" + return UuidRecognizer() + + +@pytest.fixture(scope="module") +def entities(): + """Return the entity list this recognizer supports.""" + return ["UUID"] + + +@pytest.mark.parametrize( + "text, expected_len, expected_positions, expected_score_ranges", + [ + # fmt: off + # Version 4 (random) - most common + ("Request ID: 550e8400-e29b-41d4-a716-446655440000", + 1, ((12, 48),), ((0.5, 0.5),)), + ("User UUID: 6fa459ea-ee8a-3ca4-894e-db77e160355e", + 1, ((11, 47),), ((0.5, 0.5),)), + + # Version 1 (time-based) + ("Trace: f47ac10b-58cc-1372-8567-0e02b2c3d479", + 1, ((7, 43),), ((0.5, 0.5),)), + + # Version 5 (SHA-1 name-based) + ("Object id 74738ff5-5367-5958-9aee-98fffdcd1876 created", + 1, ((10, 46),), ((0.5, 0.5),)), + + # Version 7 (timestamp-based, RFC 9562) + ("New record: 018f4f8e-9a3b-7c3d-8e9f-1a2b3c4d5e6f", + 1, ((12, 48),), ((0.5, 0.5),)), + + # Uppercase + ("GUID: 550E8400-E29B-41D4-A716-446655440000", + 1, ((6, 42),), ((0.5, 0.5),)), + + # With context keywords + ("unique identifier: 550e8400-e29b-41d4-a716-446655440000", + 1, ((19, 55),), ((0.5, "max"),)), + ("The guid is 6fa459ea-ee8a-3ca4-894e-db77e160355e", + 1, ((12, 48),), ((0.5, "max"),)), + + # Multiple UUIDs + ( + "IDs: 550e8400-e29b-41d4-a716-446655440000 and " + "f47ac10b-58cc-1372-8567-0e02b2c3d479", + 2, + ((5, 41), (46, 82)), + ((0.5, 0.5), (0.5, 0.5)), + ), + + # Invalid cases - should not match + ("Not a UUID: 550e8400-e29b-41d4-a716", + 0, (), ()), # Too short + ("Invalid: zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + 0, (), ()), # Invalid hex + ("Nil UUID: 00000000-0000-0000-0000-000000000000", + 0, (), ()), # Nil UUID filtered + ("Bad version: 550e8400-e29b-01d4-a716-446655440000", + 0, (), ()), # Invalid version nibble (0) + ("Bad version: 550e8400-e29b-91d4-a716-446655440000", + 0, (), ()), # Invalid version nibble (9) + ("Bad variant: 550e8400-e29b-41d4-1716-446655440000", + 0, (), ()), # Invalid variant nibble + # fmt: on + ], +) +def test_when_uuids_then_succeed( + text, + expected_len, + expected_positions, + expected_score_ranges, + recognizer, + entities, + max_score, +): + """Verify UuidRecognizer detects valid UUIDs and rejects invalid ones.""" + results = recognizer.analyze(text, entities) + 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 + ) From 2aa72b0bfa381867111bf4bddd925a0521c8db83 Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Mon, 20 Jul 2026 20:44:28 -0500 Subject: [PATCH 2/9] docs: add UUID to supported entities table --- docs/supported_entities.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/supported_entities.md b/docs/supported_entities.md index e04f770f0f..5dd67e8507 100644 --- a/docs/supported_entities.md +++ b/docs/supported_entities.md @@ -25,6 +25,7 @@ For more information, refer to the [adding new recognizers documentation](analyz |PHONE_NUMBER|A telephone number. The `PhoneRecognizer` can be extended programmatically for country-specific detection by configuring `supported_regions` and `supported_entity` (e.g. Philippines: `PhoneRecognizer(supported_regions=["PH"], supported_entity="PH_MOBILE_NUMBER")`, Turkey: `PhoneRecognizer(supported_regions=["TR"], supported_entity="TR_PHONE_NUMBER")`)|Custom logic, pattern match and context| |MEDICAL_LICENSE|Common medical license numbers.|Pattern match, context and checksum| |URL|A URL (Uniform Resource Locator), unique identifier used to locate a resource on the Internet|Pattern match, context and top level url validation| +|UUID|A Universally Unique Identifier (UUID) in the standard 8-4-4-4-12 hyphenated hexadecimal format. Covers RFC 4122 versions 1-5 and RFC 9562 version 7 (timestamp-based).|Pattern match, validation of version/variant nibbles, and context| ### USA From c6483e4f27ecc07de6cd6df09f9d30f9f7fd9ea3 Mon Sep 17 00:00:00 2001 From: Steve Watson <36611733+watsos@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:51:39 -0500 Subject: [PATCH 3/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- presidio-analyzer/tests/test_uuid_recognizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py index cf16ac12a2..9d449e76e7 100644 --- a/presidio-analyzer/tests/test_uuid_recognizer.py +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -20,7 +20,7 @@ def entities(): "text, expected_len, expected_positions, expected_score_ranges", [ # fmt: off - # Version 4 (random) - most common + # Versions 4 (random) and 3 (name-based, MD5) ("Request ID: 550e8400-e29b-41d4-a716-446655440000", 1, ((12, 48),), ((0.5, 0.5),)), ("User UUID: 6fa459ea-ee8a-3ca4-894e-db77e160355e", From 21d2649507c328662f8461ad80f0e86133c32b89 Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Mon, 20 Jul 2026 20:59:36 -0500 Subject: [PATCH 4/9] fix: filter empty UUID segments and export UuidRecognizer from generic package --- .../predefined_recognizers/generic/__init__.py | 2 ++ .../predefined_recognizers/generic/uuid_recognizer.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/__init__.py index 44112227c3..a8b23a3606 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/__init__.py @@ -8,6 +8,7 @@ from .mac_recognizer import MacAddressRecognizer from .phone_recognizer import PhoneRecognizer from .url_recognizer import UrlRecognizer +from .uuid_recognizer import UuidRecognizer __all__ = [ "CreditCardRecognizer", @@ -18,4 +19,5 @@ "PhoneRecognizer", "UrlRecognizer", "MacAddressRecognizer", + "UuidRecognizer", ] diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py index 682e06e4a9..0bcb73cc97 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py @@ -1,4 +1,3 @@ -import re from typing import List, Optional from presidio_analyzer import Pattern, PatternRecognizer @@ -70,7 +69,7 @@ def invalidate_result(self, pattern_text: str) -> bool: return True groups = pattern_text.split("-") - if len(groups) != 5: + if len(groups) != 5 or any(not g for g in groups): return True version_nibble = groups[2][0].lower() From 944d2864f302de6414dd9e6a2f6680fa54cd858b Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Mon, 20 Jul 2026 21:08:00 -0500 Subject: [PATCH 5/9] test: add UUID v2 (DCE Security) coverage --- presidio-analyzer/tests/test_uuid_recognizer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py index 9d449e76e7..f2d92aff25 100644 --- a/presidio-analyzer/tests/test_uuid_recognizer.py +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -20,7 +20,7 @@ def entities(): "text, expected_len, expected_positions, expected_score_ranges", [ # fmt: off - # Versions 4 (random) and 3 (name-based, MD5) + # Version 4 (random) - most common ("Request ID: 550e8400-e29b-41d4-a716-446655440000", 1, ((12, 48),), ((0.5, 0.5),)), ("User UUID: 6fa459ea-ee8a-3ca4-894e-db77e160355e", @@ -30,6 +30,10 @@ def entities(): ("Trace: f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, ((7, 43),), ((0.5, 0.5),)), + # Version 2 (DCE Security, RFC 4122) + ("DCE UUID: 550e8400-e29b-21d4-a716-446655440000", + 1, ((10, 46),), ((0.5, 0.5),)), + # Version 5 (SHA-1 name-based) ("Object id 74738ff5-5367-5958-9aee-98fffdcd1876 created", 1, ((10, 46),), ((0.5, 0.5),)), From 841e2e9b28b2d77b21654ca6f9344be5b07e29a4 Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Thu, 23 Jul 2026 09:19:47 -0500 Subject: [PATCH 6/9] feat: add UUID v6 and v8 support (RFC 9562) --- CHANGELOG.md | 2 +- docs/supported_entities.md | 2 +- .../generic/uuid_recognizer.py | 12 ++++++++---- presidio-analyzer/tests/test_uuid_recognizer.py | 8 ++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a2383fa7..7364dcdd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### Analyzer #### Added -- Added `UuidRecognizer` (generic, entity type `UUID`) to detect UUIDs in the standard 8-4-4-4-12 hyphenated hexadecimal format, covering RFC 4122 versions 1-5 and RFC 9562 version 7 (timestamp-based). Validates version and variant nibbles and filters the nil UUID to reduce false positives. +- Added `UuidRecognizer` (generic, entity type `UUID`) to detect UUIDs in the standard 8-4-4-4-12 hyphenated hexadecimal format, covering RFC 4122 versions 1-5 and RFC 9562 versions 6-8. Validates version and variant nibbles and filters the nil UUID to reduce false positives. #### Fixed - Language model recognizers (`BasicLangExtractRecognizer`, `AzureOpenAILangExtractRecognizer`) configured in a recognizer registry YAML now honour `config_path` (and other recognizer-specific kwargs). Previously these entries were validated by the strict `PredefinedRecognizerConfig` schema, which has no `config_path` field and does not allow extra keys, so `config_path` was silently dropped and the recognizer fell back to its bundled default model configuration. Added a `LangExtractRecognizerConfig` model (`extra="allow"`) and registered both recognizer class names in `CONFIG_MODEL_MAP`. diff --git a/docs/supported_entities.md b/docs/supported_entities.md index 5dd67e8507..c5c0d5dc75 100644 --- a/docs/supported_entities.md +++ b/docs/supported_entities.md @@ -25,7 +25,7 @@ For more information, refer to the [adding new recognizers documentation](analyz |PHONE_NUMBER|A telephone number. The `PhoneRecognizer` can be extended programmatically for country-specific detection by configuring `supported_regions` and `supported_entity` (e.g. Philippines: `PhoneRecognizer(supported_regions=["PH"], supported_entity="PH_MOBILE_NUMBER")`, Turkey: `PhoneRecognizer(supported_regions=["TR"], supported_entity="TR_PHONE_NUMBER")`)|Custom logic, pattern match and context| |MEDICAL_LICENSE|Common medical license numbers.|Pattern match, context and checksum| |URL|A URL (Uniform Resource Locator), unique identifier used to locate a resource on the Internet|Pattern match, context and top level url validation| -|UUID|A Universally Unique Identifier (UUID) in the standard 8-4-4-4-12 hyphenated hexadecimal format. Covers RFC 4122 versions 1-5 and RFC 9562 version 7 (timestamp-based).|Pattern match, validation of version/variant nibbles, and context| +|UUID|A Universally Unique Identifier (UUID) in the standard 8-4-4-4-12 hyphenated hexadecimal format. Covers RFC 4122 versions 1-5 and RFC 9562 versions 6-8.|Pattern match, validation of version/variant nibbles, and context| ### USA diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py index 0bcb73cc97..2013361876 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py @@ -8,8 +8,11 @@ class UuidRecognizer(PatternRecognizer): Recognize UUID (Universally Unique Identifier) using regex. Supports the standard 8-4-4-4-12 hyphenated hexadecimal format for - UUID versions 1-5 (RFC 4122) and version 7 (RFC 9562, timestamp-based, - increasingly common in modern distributed systems, databases, and logs). + UUID versions 1-8: versions 1-5 (RFC 4122, including the nil UUID + special case), version 6 (RFC 9562, reordered time-based), version 7 + (RFC 9562, unix timestamp-based, increasingly common in modern + distributed systems, databases, and logs), and version 8 + (RFC 9562, vendor/implementation-specific). ref: - https://datatracker.ietf.org/doc/html/rfc4122 @@ -26,8 +29,9 @@ class UuidRecognizer(PatternRecognizer): CONTEXT = ["uuid", "guid", "unique identifier"] - # Valid version nibble values: 1-5 (RFC 4122) and 7 (RFC 9562) - VALID_VERSIONS = {"1", "2", "3", "4", "5", "7"} + # Valid version nibble values: 1-8 (versions 1-5 per RFC 4122; + # versions 6, 7, 8 per RFC 9562) + VALID_VERSIONS = {"1", "2", "3", "4", "5", "6", "7", "8"} # Valid variant bits (RFC 4122 variant): first hex digit of the # 4th group must be 8, 9, a, or b diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py index f2d92aff25..0f9e3fd116 100644 --- a/presidio-analyzer/tests/test_uuid_recognizer.py +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -38,10 +38,18 @@ def entities(): ("Object id 74738ff5-5367-5958-9aee-98fffdcd1876 created", 1, ((10, 46),), ((0.5, 0.5),)), + # Version 6 (reordered time-based, RFC 9562) + ("Sortable ID: 1ec9414c-232a-6b00-b3c8-9e6bdeced846", + 1, ((13, 49),), ((0.5, 0.5),)), + # Version 7 (timestamp-based, RFC 9562) ("New record: 018f4f8e-9a3b-7c3d-8e9f-1a2b3c4d5e6f", 1, ((12, 48),), ((0.5, 0.5),)), + # Version 8 (vendor/implementation-specific, RFC 9562) + ("Custom UUID: 550e8400-e29b-81d4-a716-446655440000", + 1, ((13, 49),), ((0.5, 0.5),)), + # Uppercase ("GUID: 550E8400-E29B-41D4-A716-446655440000", 1, ((6, 42),), ((0.5, 0.5),)), From dc80daa2b4ba37edccf011a032f66f9ee37110d2 Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Thu, 23 Jul 2026 09:33:04 -0500 Subject: [PATCH 7/9] docs: clarify nil UUID is excluded, not a supported special case --- docs/supported_entities.md | 2 +- .../predefined_recognizers/generic/uuid_recognizer.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/supported_entities.md b/docs/supported_entities.md index 3750c48ea0..aaef27ee3d 100644 --- a/docs/supported_entities.md +++ b/docs/supported_entities.md @@ -25,7 +25,7 @@ For more information, refer to the [adding new recognizers documentation](analyz |PHONE_NUMBER|A telephone number. The `PhoneRecognizer` can be extended programmatically for country-specific detection by configuring `supported_regions` and `supported_entity` (e.g. Philippines: `PhoneRecognizer(supported_regions=["PH"], supported_entity="PH_MOBILE_NUMBER")`, Turkey: `PhoneRecognizer(supported_regions=["TR"], supported_entity="TR_PHONE_NUMBER")`)|Custom logic, pattern match and context| |MEDICAL_LICENSE|Common medical license numbers.|Pattern match, context and checksum| |URL|A URL (Uniform Resource Locator), unique identifier used to locate a resource on the Internet|Pattern match, context and top level url validation| -|UUID|A Universally Unique Identifier (UUID) in the standard 8-4-4-4-12 hyphenated hexadecimal format. Covers RFC 4122 versions 1-5 and RFC 9562 versions 6-8.|Pattern match, validation of version/variant nibbles, and context| +|UUID|A Universally Unique Identifier (UUID) in the standard 8-4-4-4-12 hyphenated hexadecimal format. Covers RFC 4122 versions 1-5 and RFC 9562 versions 6-8; the nil UUID (all zeros) is excluded as a non-identifying sentinel.|Pattern match, validation of version/variant nibbles, and context| ### USA diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py index 2013361876..00b48f9b4e 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/generic/uuid_recognizer.py @@ -8,11 +8,10 @@ class UuidRecognizer(PatternRecognizer): Recognize UUID (Universally Unique Identifier) using regex. Supports the standard 8-4-4-4-12 hyphenated hexadecimal format for - UUID versions 1-8: versions 1-5 (RFC 4122, including the nil UUID - special case), version 6 (RFC 9562, reordered time-based), version 7 - (RFC 9562, unix timestamp-based, increasingly common in modern - distributed systems, databases, and logs), and version 8 - (RFC 9562, vendor/implementation-specific). + RFC 4122 UUID versions 1-5 and RFC 9562 versions 6-8. + + Note: the nil UUID (00000000-0000-0000-0000-000000000000) is explicitly + excluded as a non-identifying sentinel value. ref: - https://datatracker.ietf.org/doc/html/rfc4122 From edf30679f7b9e2a6b82408ba6bce9ec04405dbcb Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Thu, 23 Jul 2026 09:43:41 -0500 Subject: [PATCH 8/9] test: assert expected_positions/expected_score_ranges lengths match expected_len --- presidio-analyzer/tests/test_uuid_recognizer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py index 0f9e3fd116..0bc2a00e69 100644 --- a/presidio-analyzer/tests/test_uuid_recognizer.py +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -97,6 +97,8 @@ def test_when_uuids_then_succeed( """Verify UuidRecognizer detects valid UUIDs and rejects invalid ones.""" results = recognizer.analyze(text, entities) assert len(results) == expected_len + assert len(expected_positions) == expected_len + assert len(expected_score_ranges) == expected_len for res, (st_pos, fn_pos), (st_score, fn_score) in zip( results, expected_positions, expected_score_ranges ): From f41581d0804dd945f8d11a5336f8f3d4f9412a8e Mon Sep 17 00:00:00 2001 From: Steve Watson Date: Fri, 24 Jul 2026 08:43:56 -0500 Subject: [PATCH 9/9] test: assert recognizer growth+presence instead of exact count; add UUID v3 coverage --- presidio-analyzer/tests/test_recognizer_registry.py | 8 ++++++-- presidio-analyzer/tests/test_uuid_recognizer.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index cac0f87ad1..474d10541d 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -51,11 +51,15 @@ def mock_recognizer_registry(): def test_when_get_recognizers_then_all_recognizers_returned(mock_recognizer_registry): registry = mock_recognizer_registry + count_before_loading = len(registry.get_recognizers(language="en", all_fields=True)) registry.load_predefined_recognizers() recognizers = registry.get_recognizers(language="en", all_fields=True) - # 1 custom recognizer in english + 29 predefined - 11 disabled - assert len(recognizers) == 1 + 29 - 11 + # Loading predefined recognizers should add EN recognizers, and the new + # UuidRecognizer should be among them. Avoid asserting an exact count, + # since that count changes whenever a recognizer is added or removed. + assert len(recognizers) > count_before_loading + assert any(type(rec).__name__ == "UuidRecognizer" for rec in recognizers) def test_when_get_recognizers_then_return_all_fields(mock_recognizer_registry): diff --git a/presidio-analyzer/tests/test_uuid_recognizer.py b/presidio-analyzer/tests/test_uuid_recognizer.py index 0bc2a00e69..b7cbe36cbb 100644 --- a/presidio-analyzer/tests/test_uuid_recognizer.py +++ b/presidio-analyzer/tests/test_uuid_recognizer.py @@ -34,6 +34,10 @@ def entities(): ("DCE UUID: 550e8400-e29b-21d4-a716-446655440000", 1, ((10, 46),), ((0.5, 0.5),)), + # Version 3 (MD5 name-based, RFC 4122) + ("6ba7b810-9dad-31d1-80b4-00c04fd430c8", + 1, ((0, 36),), ((0.5, 0.5),)), + # Version 5 (SHA-1 name-based) ("Object id 74738ff5-5367-5958-9aee-98fffdcd1876 created", 1, ((10, 46),), ((0.5, 0.5),)),