#!/usr/bin/env python3 """CASAN H4 PII masker driven by .specify/security/pii-rules.yaml. Reads content on stdin, applies every `action: mask` rule from the rules file, and writes the masked content to stdout. Type-specific replacement tokens are preserved so downstream evidence stays stable (***MASKED_EMAIL***, ***MASKED_PHONE***, ***MASKED_ID***). This makes pii-rules.yaml the source of truth for PII masking instead of dead config: editing/removing a rule changes runtime behavior. """ import re import sys REPLACEMENT_BY_TYPE = { "email": "***MASKED_EMAIL***", "phone": "***MASKED_PHONE***", "personal_id": "***MASKED_ID***", "address": "***MASKED_ADDRESS***", } def load_rules(path): rules, cur = [], {} with open(path, encoding="utf-8") as fh: for raw in fh: s = raw.strip() m = re.match(r"-\s*id:\s*(\S+)", s) if m: if cur: rules.append(cur) cur = {"id": m.group(1)} continue m = re.match(r'type:\s*"?([^"\s]+)"?', s) if m: cur["type"] = m.group(1) continue m = re.match(r'regex:\s*"(.*)"\s*$', s) if m: # YAML double-quoted: collapse \\ -> \ to recover the real regex. cur["regex"] = m.group(1).replace("\\\\", "\\") continue m = re.match(r"action:\s*(\S+)", s) if m: cur["action"] = m.group(1) continue if cur: rules.append(cur) return rules def main(): data = sys.stdin.read() if len(sys.argv) < 2: sys.stdout.write(data) return try: rules = load_rules(sys.argv[1]) except OSError: sys.stdout.write(data) return for rule in rules: if rule.get("action") != "mask" or "regex" not in rule: continue token = REPLACEMENT_BY_TYPE.get(rule.get("type", ""), "***MASKED***") try: data = re.sub(rule["regex"], token, data) except re.error: continue sys.stdout.write(data) if __name__ == "__main__": main()