Source code for imednet.utils.security
"""Security utilities."""
import re
from typing import Any
from imednet.errors import ClientError, PathTraversalValidationError
_WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^[A-Za-z]:[/\\]")
[docs]class SensitivityRegistry:
"""Registry for managing sensitive fields (PHI) that should be masked in logs."""
[docs] def __init__(self) -> None:
"""Initialize the registry with default sensitive and exempt keys."""
# Default clinical PHI fields
self._sensitive_keys = {
"patient_name",
"patient_initials",
"dob",
"date_of_birth",
"ssn",
"phone",
"email",
"address",
"birth_date",
}
# Keys explicitly exempted from masking
self._exempt_keys = {"subject_key", "study_key", "record_id"}
[docs] def add_sensitive_key(self, key: str) -> None:
"""Add a key to the sensitive list if it is not exempted."""
if key not in self._exempt_keys:
self._sensitive_keys.add(key)
[docs] def remove_sensitive_key(self, key: str) -> None:
"""Remove a key from the sensitive list."""
self._sensitive_keys.discard(key)
[docs] def add_exempt_key(self, key: str) -> None:
"""Add a key to the exemption list."""
self._exempt_keys.add(key)
[docs] def remove_exempt_key(self, key: str) -> None:
"""Remove a key from the exemption list."""
self._exempt_keys.discard(key)
[docs] def is_sensitive(self, key: str) -> bool:
"""Check if a key is considered sensitive and not exempted."""
if key in self._exempt_keys:
return False
return key in self._sensitive_keys
global_sensitivity_registry = SensitivityRegistry()
[docs]def mask_clinical_phi(value: Any) -> Any:
"""Recursively mask sensitive keys in unstructured data."""
if isinstance(value, dict):
return {
k: (
"***MASKED***"
if global_sensitivity_registry.is_sensitive(k)
else mask_clinical_phi(v)
)
for k, v in value.items()
}
if isinstance(value, list):
return [mask_clinical_phi(v) for v in value]
if isinstance(value, tuple):
return tuple(mask_clinical_phi(v) for v in value)
return value
[docs]def validate_partition_key(key: str) -> None:
"""Reject partition keys that could escape or reshape the target directory."""
if "\x00" in key:
raise PathTraversalValidationError(f"Partition key must not contain null bytes: {key!r}")
if key.startswith(("/", "\\")) or _WINDOWS_ABSOLUTE_PATH_RE.match(key):
raise PathTraversalValidationError(
f"Partition key must not use absolute path modifiers: {key!r}"
)
if "../" in key or "..\\" in key or key == "..":
raise PathTraversalValidationError(
f"Partition key must not contain parent-directory traversal: {key!r}"
)
if "/" in key or "\\" in key:
raise PathTraversalValidationError(
f"Partition key must not contain path separators: {key!r}"
)