# permission_gate.py — 実行前に契約を引き、判定と理由を監査ログへ残すimport json, time, fnmatch, sys, pathlib, yamlPOLICY = yaml.safe_load(pathlib.Path("unattended-permission.yaml").read_text())AUDIT = pathlib.Path("audit/permission-events.jsonl")def classify(action: str, **attrs) -> tuple[str, dict | None]: for rule in POLICY["rules"]: m = rule["match"] if m.get("action") != action: continue ok = True for key, pat in m.items(): if key == "action": continue val = attrs.get(key.replace("_glob", "").replace("_prefix", ""), "") if key.endswith("_glob") and not fnmatch.fnmatch(val, pat): ok = False if key.endswith("_prefix") and not any(val.startswith(p) for p in pat): ok = False if ok: return rule["decision"], rule return POLICY["default"], None # 未分類は既定(queue)へdef gate(action: str, **attrs) -> bool: decision, rule = classify(action, **attrs) AUDIT.parent.mkdir(exist_ok=True) with AUDIT.open("a") as f: f.write(json.dumps({ "ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "action": action, "attrs": attrs, "decision": decision, "matched": rule["match"] if rule else "default", }, ensure_ascii=False) + "\n") return decision == "allow" # queue / deny はここでは実行させない
# digest.py — 前夜の監査ログを、判定ごとに1枚へ畳むimport json, collections, pathlibevents = [json.loads(l) for l in pathlib.Path("audit/permission-events.jsonl").read_text().splitlines()]by = collections.defaultdict(list)for e in events: by[e["decision"]].append(e)print(f"# 前夜のパーミッション判定 計 {len(events)} 件")for d in ("queue", "deny", "allow"): items = by.get(d, []) print(f"\n## {d}: {len(items)} 件") for e in items[:20] if d != "allow" else []: print(f"- {e['ts']} {e['action']} {e['attrs']}")