← Files Oximy Reality ChecksARCHIVED FILE
skills/safe-to-paste/scripts/redact_text.py
3.92 KB · Oct 2, 2026 · 00:34 UTC
#!/usr/bin/env python3
"""Detect and consistently tokenize common sensitive strings in text files."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
MAX_BYTES = 20 * 1024 * 1024
PATTERNS = (
("private-key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
("github-token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,255}\b")),
("slack-token", re.compile(r"\bxox(?:b|p|a|r|s)-[A-Za-z0-9-]{10,200}\b")),
("aws-access-key", re.compile(r"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b")),
("jwt", re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b")),
("generic-api-key", re.compile(r"\bsk-[A-Za-z0-9_-]{20,200}\b")),
("email", re.compile(r"(?<![\w.+-])[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}(?![\w-])", re.IGNORECASE)),
("ssn-us", re.compile(r"(?<!\d)(?!000|666|9\d\d)\d{3}[- ]?(?!00)\d{2}[- ]?(?!0000)\d{4}(?!\d)")),
("ipv4", re.compile(r"(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)(?!\d)")),
)
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def load_text(path: Path) -> tuple[bytes, str]:
raw = path.read_bytes()
if len(raw) > MAX_BYTES:
raise ValueError(f"file exceeds {MAX_BYTES} bytes")
if b"\x00" in raw:
raise ValueError("binary or office files require format-specific inspection")
return raw, raw.decode("utf-8")
def redact(text: str) -> tuple[str, list[dict]]:
tokens: dict[tuple[str, str], str] = {}
counts: Counter[str] = Counter()
lines: dict[str, set[int]] = defaultdict(set)
result = text
for category, pattern in PATTERNS:
def replace(match: re.Match[str]) -> str:
value = match.group(0)
key = (category, value)
if key not in tokens:
tokens[key] = f"[{category.upper().replace('-', '_')}_{sum(1 for c, _ in tokens if c == category) + 1}]"
counts[category] += 1
lines[category].add(text.count("\n", 0, match.start()) + 1)
return tokens[key]
result = pattern.sub(replace, result)
findings = [
{"category": category, "count": counts[category], "lines": sorted(lines[category])}
for category in sorted(counts)
]
return result, findings
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("--output", type=Path, help="write a redacted copy; source overwrite is refused")
parser.add_argument("--report", type=Path, help="write the JSON report instead of stdout")
args = parser.parse_args()
try:
source_raw, source = load_text(args.input)
transformed, findings = redact(source)
report = {
"source": str(args.input.resolve()),
"source_sha256": digest(source_raw),
"scanner_scope": [category for category, _ in PATTERNS],
"findings": findings,
"finding_count": sum(item["count"] for item in findings),
"semantic_review_required": True,
}
if args.output:
if args.output.resolve() == args.input.resolve():
raise ValueError("refusing to overwrite source")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(transformed)
report["output"] = str(args.output.resolve())
report["output_sha256"] = digest(transformed.encode())
rendered = json.dumps(report, indent=2) + "\n"
if args.report:
args.report.write_text(rendered)
else:
sys.stdout.write(rendered)
return 0
except (OSError, UnicodeDecodeError, ValueError) as error:
print(f"redact_text: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 6ce5438f80e0faa369bfdde7c88da2b614cb967a9d49a410140eeb8baff1dceb