85 lines
2.0 KiB
Python
85 lines
2.0 KiB
Python
"""
|
|
©AngelaMos | 2026
|
|
redaction.py
|
|
"""
|
|
|
|
|
|
from dlp_scanner.constants import RedactionStyle
|
|
|
|
|
|
REDACTED_LABEL: str = "[REDACTED]"
|
|
MASK_CHAR: str = "*"
|
|
SNIPPET_CONTEXT_CHARS: int = 20
|
|
|
|
|
|
def redact(
|
|
text: str,
|
|
start: int,
|
|
end: int,
|
|
style: RedactionStyle = "partial",
|
|
) -> str:
|
|
"""
|
|
Redact matched text according to the chosen strategy
|
|
"""
|
|
matched = text[start : end]
|
|
|
|
if style == "none":
|
|
return _build_snippet(text, start, end, matched)
|
|
|
|
if style == "full":
|
|
return _build_snippet(text, start, end, REDACTED_LABEL)
|
|
|
|
redacted = _partial_redact(matched)
|
|
return _build_snippet(text, start, end, redacted)
|
|
|
|
|
|
def _partial_redact(value: str) -> str:
|
|
"""
|
|
Partially mask a value, keeping the last few chars visible
|
|
"""
|
|
stripped = value.replace("-", "").replace(" ", "")
|
|
|
|
if len(stripped) >= 9 and stripped.isdigit():
|
|
return MASK_CHAR * (len(value) - 4) + value[-4 :]
|
|
|
|
if "@" in value:
|
|
local, domain = value.rsplit("@", maxsplit = 1)
|
|
masked_local = local[0] + MASK_CHAR * (len(local) - 1)
|
|
return f"{masked_local}@{domain}"
|
|
|
|
if len(value) > 8:
|
|
visible = max(4, len(value) // 4)
|
|
return (MASK_CHAR * (len(value) - visible) + value[-visible :])
|
|
|
|
return MASK_CHAR * len(value)
|
|
|
|
|
|
def _build_snippet(
|
|
text: str,
|
|
start: int,
|
|
end: int,
|
|
replacement: str,
|
|
) -> str:
|
|
"""
|
|
Build a snippet with context around the redacted match
|
|
"""
|
|
context_start = max(0, start - SNIPPET_CONTEXT_CHARS)
|
|
context_end = min(len(text), end + SNIPPET_CONTEXT_CHARS)
|
|
|
|
prefix = text[context_start : start]
|
|
suffix = text[end : context_end]
|
|
|
|
prefix = prefix.replace("\n", " ").strip()
|
|
suffix = suffix.replace("\n", " ").strip()
|
|
|
|
parts: list[str] = []
|
|
if context_start > 0:
|
|
parts.append("...")
|
|
parts.append(prefix)
|
|
parts.append(replacement)
|
|
parts.append(suffix)
|
|
if context_end < len(text):
|
|
parts.append("...")
|
|
|
|
return "".join(parts)
|