Cybersecurity-Projects/PROJECTS/intermediate/dlp-scanner/src/dlp_scanner/redaction.py

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)