headless / scripts /intake_filter.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
6.47 kB
from __future__ import annotations
# llm-wiki-version: 2.0.0
# runtime: dev-only
"""
intake_filter.py — v2 intake privacy filter.
Rohit G00 v2 范式:raw 摄入前脱敏,wiki 写入时按 audience 分级。
v2.0 实现:
1. 读 manifests/redact_rules.yaml(每条 = name + pattern + replacement)
2. 扫 raw 根目录(默认 ../<project>_raw/,可 --raw-root 覆盖)
3. 对每个 .md/.txt/.csv/.json 文件应用正则替换
4. 在 manifests/redaction_log.md 写日志(每文件命中几条)
5. 退出口为 0(仅警告),CI 默认不阻塞
Usage:
python3 scripts/intake_filter.py
python3 scripts/intake_filter.py --dry-run
python3 scripts/intake_filter.py --raw-root ../my_raw
python3 scripts/intake_filter.py --rules manifests/redact_rules.yaml
"""
import argparse
import os
import re
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_RULES = ROOT / "manifests" / "redact_rules.yaml"
DEFAULT_LOG = ROOT / "manifests" / "redaction_log.md"
# 默认 deny extensions / max size (写在脚本里,不进 rules.yaml 避免 YAML 解析复杂)
DEFAULT_DENY_EXT = {".zip", ".rar", ".7z", ".tar", ".gz"}
DEFAULT_MAX_SIZE_MB = 200
# 10 行内建 PII 正则(无外部依赖)
BUILTIN_PATTERNS = [
("phone_cn", re.compile(r"1[3-9]\d{9}"), "[PHONE]"),
("email", re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), "[EMAIL]"),
("id_card_cn", re.compile(r"\d{17}[\dXx]"), "[ID]"),
("credit_card", re.compile(r"\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}"), "[CC]"),
]
def parse_yaml_rules(path: Path) -> list[tuple[str, re.Pattern, str]]:
"""Minimal YAML parser: each rule is a block of 3 lines (name/pattern/replacement)."""
if not path.exists():
return []
rules: list[tuple[str, re.Pattern, str]] = []
block: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
line_stripped = line.strip()
if not line_stripped or line_stripped.startswith("#"):
continue
if line_stripped.startswith("- name:"):
if block:
_flush(block, rules)
block = {"name": line_stripped.split(":", 1)[1].strip()}
elif line_stripped.startswith("pattern:") and block:
block["pattern"] = line_stripped.split(":", 1)[1].strip().strip('"').strip("'")
elif line_stripped.startswith("replacement:") and block:
block["replacement"] = line_stripped.split(":", 1)[1].strip().strip('"').strip("'")
if block:
_flush(block, rules)
return rules
def _flush(block: dict[str, str], rules: list) -> None:
name = block.get("name", "unnamed")
pat = block.get("pattern", "")
repl = block.get("replacement", "[REDACTED]")
if not pat:
return
try:
rules.append((name, re.compile(pat), repl))
except re.error:
pass # 跳过非法正则
def main() -> int:
parser = argparse.ArgumentParser(description="v2 intake privacy filter")
parser.add_argument("--raw-root", type=Path,
default=lambda: Path(os.environ.get("PROJECT_RAW_ROOT", ROOT.parent / "_raw")),
help="raw root directory to scan (env PROJECT_RAW_ROOT or --raw-root override; default: ../_raw)")
parser.add_argument("--rules", type=Path, default=DEFAULT_RULES,
help="redact_rules.yaml path (optional, falls back to builtins)")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--log", type=Path, default=DEFAULT_LOG)
args = parser.parse_args()
if not args.raw_root.exists():
print(f"intake_filter: raw_root {args.raw_root} does not exist, skipping")
return 0
# Load rules: yaml first, then builtins
rules = parse_yaml_rules(args.rules) or BUILTIN_PATTERNS
print(f"intake_filter: {len(rules)} patterns loaded ({'yaml' if args.rules.exists() else 'builtin'})")
# Scan
hits: list[dict] = []
scanned = 0
skipped = 0
for path in args.raw_root.rglob("*"):
if not path.is_file():
continue
scanned += 1
if path.suffix.lower() in DEFAULT_DENY_EXT:
skipped += 1
continue
size_mb = path.stat().st_size / (1024 * 1024)
if size_mb > DEFAULT_MAX_SIZE_MB:
skipped += 1
continue
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
file_hits: dict[str, int] = {}
new_text = text
for name, pat, repl in rules:
count = len(pat.findall(new_text))
if count > 0:
file_hits[name] = count
new_text = pat.sub(repl, new_text)
if file_hits:
if not args.dry_run:
# 仅当命中数 > 0 时回写
path.write_text(new_text, encoding="utf-8")
hits.append({"file": str(path.relative_to(args.raw_root)), "hits": file_hits})
# Write log
today = date.today().isoformat()
log_lines = [f"# Intake Redaction Log", "",
f"- date: {today}",
f"- raw_root: {args.raw_root}",
f"- rules: {args.rules} ({'yaml' if args.rules.exists() else 'builtin'})",
f"- dry_run: {args.dry_run}",
f"- scanned: {scanned}, skipped: {skipped}, modified: {len(hits)}",
""]
if hits:
log_lines.append("## Files modified")
log_lines.append("")
for h in hits:
log_lines.append(f"- `{h['file']}`: " + ", ".join(f"{k}={v}" for k, v in h["hits"].items()))
else:
log_lines.append("## No PII / redact patterns matched.")
if not args.dry_run:
args.log.parent.mkdir(parents=True, exist_ok=True)
args.log.write_text("\n".join(log_lines) + "\n", encoding="utf-8")
# Report
print(f"- scanned: {scanned}")
print(f"- skipped: {skipped} (deny ext or >{DEFAULT_MAX_SIZE_MB}MB)")
print(f"- modified: {len(hits)}")
if hits:
for h in hits[:10]:
print(f" - {h['file']}: " + ", ".join(f"{k}={v}" for k, v in h["hits"].items()))
if len(hits) > 10:
print(f" ... and {len(hits) - 10} more")
if not args.dry_run:
print(f"- log: {args.log}")
else:
print("- dry-run: no files written")
return 0
if __name__ == "__main__":
sys.exit(main())