diff --git a/docs/changes/CUST-WP-0080/activate_secret_hooks.py b/docs/changes/CUST-WP-0080/activate_secret_hooks.py new file mode 100644 index 00000000..e09f6a23 --- /dev/null +++ b/docs/changes/CUST-WP-0080/activate_secret_hooks.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Apply/verify or roll back the exact reviewed local hook proposal. + +No production credential inputs, repository source/index writes, Git config +changes or remote calls. Synthetic probes use alternate indexes/object stores. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import signal +import stat +import subprocess +import tempfile + + +class Refused(Exception): + pass + + +def digest(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def safe_regular(path): + path = Path(path) + if path.is_symlink() or not path.is_file(): raise Refused('file-shape-refused') + if path.stat().st_uid != os.getuid(): raise Refused('file-owner-refused') + return path + + +def atomic(path, data, mode): + path = Path(path) + fd, temporary = tempfile.mkstemp(prefix='.hook-activation-', dir=path.parent) + try: + with os.fdopen(fd, 'wb') as output: + output.write(data); output.flush(); os.fsync(output.fileno()) + os.chmod(temporary, mode) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): os.unlink(temporary) + + +def run(args, cwd=None, env=None, payload=None): + result = subprocess.run(args, cwd=cwd, env=env, input=payload, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=60, check=False) + if result.returncode: raise Refused('preparation-command-failed') + return result.stdout + + +def clean_git_env(): + env = dict(os.environ) + for key in ('GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES'): + env.pop(key, None) + return env + + +def route(root): + env = clean_git_env() + actual = run(['git', 'rev-parse', '--show-toplevel'], root, env).decode().strip() + if actual != root: raise Refused('checkout-root-changed') + name = run(['git', 'rev-parse', '--git-path', 'hooks/pre-commit'], root, env).decode().strip() + path = Path(name) + return str(path if path.is_absolute() else Path(root) / path) + + +def validate(proposal, applied=False): + if digest(__file__) != proposal['executor_sha256']: + raise Refused('executor-revision-changed') + expected = {row['repo']: row['effective_pre_commit'] for row in proposal['original_routes']} + for root in proposal['scope_roots']: + if route(root) != expected[Path(root).name]: raise Refused('hook-route-changed') + for item in proposal['hooks']: + safe_regular(item['path']) + field = 'after_sha256' if applied else 'before_sha256' + if digest(item['path']) != item[field]: raise Refused('hook-revision-changed') + if digest(item['backup']) != item['before_sha256']: raise Refused('backup-changed') + for item in proposal['protected_files']: + if digest(item['path']) != item['sha256']: raise Refused('dependency-or-other-hook-changed') + safe_regular(proposal['hook_source']) + if digest(proposal['hook_source']) != proposal['hooks'][0]['after_sha256']: + raise Refused('candidate-hook-changed') + binary = proposal['binary'] + if digest(safe_regular(binary['source'])) != binary['sha256']: + raise Refused('scanner-source-changed') + target = Path(binary['target']) + for ancestor in (target, *target.parents): + if ancestor.is_symlink(): raise Refused('scanner-path-redirected') + if target.exists() and digest(safe_regular(target)) != binary['sha256']: + raise Refused('scanner-target-conflict') + + +def probe(proposal): + results = [] + scanner = Path(proposal['binary']['target']) + original_mode = stat.S_IMODE(scanner.stat().st_mode) + with tempfile.TemporaryDirectory(prefix='rmgr-hook-proof-') as temporary: + area = Path(temporary) + key = area / 'disposable-key' + run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(key)]) + synthetic = key.read_bytes() + cases = [] + for number, root in enumerate(proposal['scope_roots']): + local = area / str(number); local.mkdir(); objects = local / 'objects'; objects.mkdir() + env = clean_git_env() + original_objects = run(['git', 'rev-parse', '--git-path', 'objects'], root, env).decode().strip() + if not Path(original_objects).is_absolute(): original_objects = str(Path(root) / original_objects) + env.update(GIT_INDEX_FILE=str(local / 'index'), GIT_OBJECT_DIRECTORY=str(objects), + GIT_ALTERNATE_OBJECT_DIRECTORIES=original_objects) + hook = route(root) + def stage(data, name='__rmgr_security_synthetic_probe__.txt'): + run(['git', 'read-tree', 'HEAD'], root, env) + oid = run(['git', 'hash-object', '-w', '--stdin'], root, env, data).decode().strip() + run(['git', 'update-index', '--add', '--cacheinfo', '100644,' + oid + ',' + name], root, env) + def invoke(): + result = subprocess.run([hook], cwd=root, env=env, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=60, check=False) + if synthetic.strip() in result.stdout + result.stderr: + raise Refused('synthetic-content-disclosed') + return result + stage(b'print("synthetic clean input")\n') + clean = invoke() + if clean.returncode != 0 or json.loads(clean.stdout).get('status') != 'clean': + raise Refused('clean-native-probe-failed') + stage(synthetic) + blocked = invoke() + if blocked.returncode != 1 or json.loads(blocked.stdout).get('status') != 'blocked': + raise Refused('secret-native-probe-failed') + stage(b'synthetic generated-only change\n', '.custodian-brief.md') + generated = invoke() + if generated.returncode != 1 or b'Generated-only commits rejected' not in generated.stderr: + raise Refused('generated-guard-native-probe-failed') + stage(b'print("synthetic clean input")\n') + cases.append((root, hook, env)) + results.append({'repo': Path(root).name, 'clean_exit': 0, 'secret_exit': 1, + 'generated_only_exit': 1, 'hook_sha256': digest(hook)}) + # Brief, reviewed failure injection; restore even on SIGTERM/INT/HUP. + try: + scanner.chmod(0o644) + for row, (root, hook, env) in zip(results, cases): + result = subprocess.run([hook], cwd=root, env=env, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=60, check=False) + body = json.loads(result.stdout) + if result.returncode != 2 or body.get('status') != 'unverified': + raise Refused('scanner-failure-native-probe-failed') + row['scanner_failure_exit'] = 2 + row['scanner_failure_reason'] = body.get('reason') + # An unreviewed checkout keeps its existing generated-only behavior. + outside = area / 'outside-profile'; outside.mkdir() + env = clean_git_env() + run(['git', 'init', '-q'], outside, env) + (outside / 'source.py').write_text('print("clean outside profile")\n') + run(['git', 'add', 'source.py'], outside, env) + path = run(['git', 'rev-parse', '--git-path', 'hooks/pre-commit'], outside, env).decode().strip() + result = subprocess.run([path], cwd=outside, env=env, capture_output=True, timeout=30, check=False) + if result.returncode != 0: raise Refused('unreviewed-checkout-behavior-changed') + finally: + scanner.chmod(original_mode) + return {'repositories': results, 'unreviewed_checkout_preserved': True, + 'indexes_and_objects': 'temporary alternate stores only', + 'scanner_mode_restored': stat.S_IMODE(scanner.stat().st_mode) == original_mode} + + +def restore(proposal): + for item in proposal['hooks']: + current = digest(safe_regular(item['path'])) + if current not in (item['before_sha256'], item['after_sha256']): + raise Refused('rollback-target-changed') + if digest(item['backup']) != item['before_sha256']: raise Refused('backup-changed') + for item in proposal['hooks']: + atomic(item['path'], Path(item['backup']).read_bytes(), item['before_mode']) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--proposal', type=Path, required=True) + parser.add_argument('--receipt', type=Path, required=True) + parser.add_argument('--apply', action='store_true') + parser.add_argument('--rollback', action='store_true') + args = parser.parse_args() + proposal = json.loads(args.proposal.read_text()) + if args.rollback: + restore(proposal) + print('Restored exact prior hooks; Git routing unchanged.') + return + validate(proposal) + if not args.apply: + print('Proposal hashes, dependencies, backups and exact checkout routes verified.') + return + changed = False + receipt = {'proposal_id': proposal['proposal_id'], 'proposal_sha256': digest(args.proposal), + 'executor_sha256': digest(__file__), 'result': 'unverified', 'cost_eur': 0} + try: + target = Path(proposal['binary']['target']) + target.parent.mkdir(parents=True, exist_ok=True) + if not target.exists(): atomic(target, Path(proposal['binary']['source']).read_bytes(), 0o755) + for item in proposal['hooks']: + changed = True + atomic(item['path'], Path(proposal['hook_source']).read_bytes(), 0o755) + validate(proposal, applied=True) + receipt['verification'] = probe(proposal) + validate(proposal, applied=True) + receipt['result'] = 'verified' + except BaseException: + if changed: + restore(proposal) + receipt['rollback'] = 'prior hooks restored' + raise + finally: + args.receipt.write_text(json.dumps(receipt, indent=2) + '\n') + print(f"Verified local scanner enforcement for {len(proposal['scope_roots'])} scoped checkouts; no Git routing changed.") + + +if __name__ == '__main__': + def interrupted(*_): raise Refused('interrupted') + for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP): signal.signal(sig, interrupted) + try: + main() + except (Refused, OSError, ValueError, subprocess.SubprocessError): + raise SystemExit('Activation refused or failed; inspect the value-free receipt and exact rollback state.') diff --git a/docs/changes/CUST-WP-0080/secret-hooks-pre-commit b/docs/changes/CUST-WP-0080/secret-hooks-pre-commit new file mode 100644 index 00000000..ea0d5164 --- /dev/null +++ b/docs/changes/CUST-WP-0080/secret-hooks-pre-commit @@ -0,0 +1,9 @@ +#!/bin/sh +# CUST-WP-0080 / RMGR-WP-0021: preserve generated-only guard and exact local scan scope. +set -eu +python3 /home/worsch/the-custodian/scripts/check_generated_only_commit.py --staged +case "$(git rev-parse --show-toplevel)" in + /home/worsch/access-engine|/home/worsch/approval-engine|/home/worsch/audit-core|/home/worsch/gate-house|/home/worsch/glas-harness|/home/worsch/key-cape|/home/worsch/kings-guard|/home/worsch/maturity-engine|/home/worsch/net-kingdom|/home/worsch/ops-mason|/home/worsch/ops-warden|/home/worsch/policy-nexus|/home/worsch/railiance-forge|/home/worsch/railiance-platform|/home/worsch/repo-manager|/home/worsch/role-engine|/home/worsch/secrets-engine|/home/worsch/security-canon|/home/worsch/tenant-engine|/home/worsch/the-custodian|/home/worsch/user-engine|/home/worsch/zone-engine) + exec python3 /home/worsch/repo-manager/src/repo_manager/secret_scan.py --path . --binary /home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks + ;; +esac diff --git a/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-0 b/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-0 new file mode 100644 index 00000000..dc48599d --- /dev/null +++ b/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-0 @@ -0,0 +1,4 @@ +#!/bin/sh +# Custodian shared generated-commit guard; preserve other hooks. +set -eu +exec python3 /home/worsch/the-custodian/scripts/check_generated_only_commit.py --staged diff --git a/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-1 b/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-1 new file mode 100644 index 00000000..a8266a83 --- /dev/null +++ b/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-1 @@ -0,0 +1,5 @@ +#!/bin/sh +# Install as .git/hooks/pre-commit; rejects generated/evidence-only auto commits too. +set -eu +REPO_ROOT="$(git rev-parse --show-toplevel)" +exec python3 "$REPO_ROOT/scripts/check_generated_only_commit.py" --staged diff --git a/docs/changes/CUST-WP-0080/secret-hooks-proposal.json b/docs/changes/CUST-WP-0080/secret-hooks-proposal.json new file mode 100644 index 00000000..c680a571 --- /dev/null +++ b/docs/changes/CUST-WP-0080/secret-hooks-proposal.json @@ -0,0 +1,329 @@ +{ + "proposal_id": "CUST80-G07-local-hooks-001", + "scope_roots": [ + "/home/worsch/access-engine", + "/home/worsch/approval-engine", + "/home/worsch/audit-core", + "/home/worsch/gate-house", + "/home/worsch/glas-harness", + "/home/worsch/key-cape", + "/home/worsch/kings-guard", + "/home/worsch/maturity-engine", + "/home/worsch/net-kingdom", + "/home/worsch/ops-mason", + "/home/worsch/ops-warden", + "/home/worsch/policy-nexus", + "/home/worsch/railiance-forge", + "/home/worsch/railiance-platform", + "/home/worsch/repo-manager", + "/home/worsch/role-engine", + "/home/worsch/secrets-engine", + "/home/worsch/security-canon", + "/home/worsch/tenant-engine", + "/home/worsch/the-custodian", + "/home/worsch/user-engine", + "/home/worsch/zone-engine" + ], + "binary": { + "source": "/tmp/netkingdom-secret-detection/gitleaks", + "target": "/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks", + "sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509" + }, + "hook_source": "/home/worsch/the-custodian/docs/changes/CUST-WP-0080/secret-hooks-pre-commit", + "hooks": [ + { + "path": "/home/worsch/repo-manager/.githooks/pre-commit", + "before_sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "before_mode": 493, + "backup": "/home/worsch/the-custodian/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-0", + "after_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-commit", + "before_sha256": "4d23af188d7891209f032123508893d2bc49a1709644278a0f1fa9008b0b187f", + "before_mode": 493, + "backup": "/home/worsch/the-custodian/docs/changes/CUST-WP-0080/secret-hooks-prior-hook-1", + "after_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd" + } + ], + "original_routes": [ + { + "repo": "security-canon", + "head": "1664a6def20be2759a1e575024563efd27c9f36f", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "net-kingdom", + "head": "19a1c75d309991de8654c4b7a9eeffea8fb90b18", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "gate-house", + "head": "bc890c4d050a2e726ce8988a8260ab9caea133e3", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "ops-mason", + "head": "d86ad141befb6800468f5132d4c7c9ce2f852bb5", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "ops-warden", + "head": "6d2f1be9691b19e826898a2166ae4451e4802ae2", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "key-cape", + "head": "1344fc80e56315c5c99f58993f9ee02ce79e8a76", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "user-engine", + "head": "85a419d6c5d0b4434d3ca7f33bde3092f5838f79", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "tenant-engine", + "head": "1dabbcf47d87d27d118dcf3147d9e3588a807e0c", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "role-engine", + "head": "d231241dbfeeb541b34243f4022883aa6417a1eb", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "zone-engine", + "head": "b5a6f88a82b125ebc82c0484bec1ea541e4f95d0", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "access-engine", + "head": "16ca97b2776789f730cc7049a2d6533612faaccd", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "approval-engine", + "head": "e1649617e8ad321f79632d5fd33e973ae5d5ae51", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "secrets-engine", + "head": "d379a3dfba73c42cd97f4d9cba3a02d705155e4d", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "audit-core", + "head": "f1e715326b3ffd956623a84fa7ab0ce5ab3fbcf9", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "kings-guard", + "head": "af02e2547598d6ec0e83f482d9ea3ef943ea67b7", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "maturity-engine", + "head": "2f1de542cc8f24a79452f906d99f95be39d8b6a9", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "policy-nexus", + "head": "f784f017b0822f34c785f0b34c7c55fcf39c9d99", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "railiance-platform", + "head": "7c79293831e2f46ad93f2ef689d9129b2ac0e139", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "glas-harness", + "head": "5ce4f38aedfd27cc783131f18480df02e2951514", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "repo-manager", + "head": "78d91efc76ddaf778365e445dc8fb5b6c924a274", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "railiance-forge", + "head": "e395e5e9c196fe7cea9cd2be513554e493e675ee", + "effective_pre_commit": "/home/worsch/repo-manager/.githooks/pre-commit", + "executable": true, + "sha256": "03a46897ea61589fe111316e561a98b423028375a1cddd1152b808df1fbeb4b5", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + }, + { + "repo": "the-custodian", + "head": "e2dbe1267ffea38cd8042c153c942b26f6d177a4", + "effective_pre_commit": "/home/worsch/the-custodian/.git/hooks/pre-commit", + "executable": true, + "sha256": "4d23af188d7891209f032123508893d2bc49a1709644278a0f1fa9008b0b187f", + "observed_control": "generated-only commit guard; no scanner invocation", + "scanner_activation": "not activated by this repair" + } + ], + "expected_result": "Synthetic clean source accepted, synthetic private key rejected, unavailable scanner refused on all 22 exact roots; generated-only guard/provenance and routing preserved.", + "verification": "Alternate Git indexes and temporary object directories; no user index, source file, branch or production credential mutation. Brief native scanner non-executable probe restores mode in finally.", + "rollback": "Restore exact prior hook bytes/modes after checking installed hashes; leave admitted scanner inert. No Git configuration changed.", + "cost_eur": 0, + "risk": "Local commits to selected checkouts may be refused on detections or verification failure. Scanner mode fault probe briefly refuses concurrent commits; no runtime service changes.", + "protected_files": [ + { + "path": "/home/worsch/repo-manager/src/repo_manager/secret_scan.py", + "sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d" + }, + { + "path": "/home/worsch/the-custodian/scripts/check_generated_only_commit.py", + "sha256": "1b372740a3265bbaaef254582be3eb116a8659f2efffccd5a26ec1c9d8db7105" + }, + { + "path": "/home/worsch/repo-manager/.githooks/prepare-commit-msg", + "sha256": "6640924b9ba281e1fc413802cff91c808580b323f11e60d1fc71f4d5cd13bd45" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-rebase.sample", + "sha256": "4febce867790052338076f4e66cc47efb14879d18097d1d61c8261859eaaa7b3" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/fsmonitor-watchman.sample", + "sha256": "e0549964e93897b519bd8e333c037e51fff0f88ba13e086a331592bf801fa1d0" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/update.sample", + "sha256": "8d5f2fa83e103cf08b57eaa67521df9194f45cbdbcb37da52ad586097a14d106" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/post-update.sample", + "sha256": "81765af2daef323061dcbc5e61fc16481cb74b3bac9ad8a174b186523586f6c5" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/push-to-checkout.sample", + "sha256": "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-applypatch.sample", + "sha256": "e15c5b469ea3e0a695bea6f2c82bcf8e62821074939ddd85b77e0007ff165475" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/post-commit", + "sha256": "da2461970c32ee3ca9be48993a15476e368184622b008732c202b9c8a35840e6" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-merge-commit.sample", + "sha256": "d3825a70337940ebbd0a5c072984e13245920cdf8898bd225c8d27a6dfc9cb53" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/commit-msg.sample", + "sha256": "1f74d5e9292979b573ebd59741d46cb93ff391acdd083d340b94370753d92437" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-push.sample", + "sha256": "ecce9c7e04d3f5dd9d8ada81753dd1d549a9634b26770042b58dda00217d086a" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/prepare-commit-msg.sample", + "sha256": "e9ddcaa4189fddd25ed97fc8c789eca7b6ca16390b2392ae3276f0c8e1aa4619" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-commit.sample", + "sha256": "f9af7d95eb1231ecf2eba9770fedfa8d4797a12b02d7240e98d568201251244a" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/sendemail-validate.sample", + "sha256": "44ebfc923dc5466bc009602f0ecf067b9c65459abfe8868ddc49b78e6ced7a92" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/pre-receive.sample", + "sha256": "a4c3d2b9c7bb3fd8d1441c31bd4ee71a595d66b44fcf49ddb310252320169989" + }, + { + "path": "/home/worsch/the-custodian/.git/hooks/applypatch-msg.sample", + "sha256": "0223497a0b8b033aa58a3a521b8629869386cf7ab0e2f101963d328aa62193f7" + } + ], + "executor_sha256": "1935cc442026a36db8fecc7f58f779dc877a79b6b2e987f8ab074138230ff74f" +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/sec-dsm-requirement-ledger.json b/docs/evidence/2026-10-12-netkingdom-security/sec-dsm-requirement-ledger.json index f523cf75..273d56bf 100644 --- a/docs/evidence/2026-10-12-netkingdom-security/sec-dsm-requirement-ledger.json +++ b/docs/evidence/2026-10-12-netkingdom-security/sec-dsm-requirement-ledger.json @@ -841,11 +841,12 @@ "closure_evidence": [], "assessment": { "scope": "Repo Manager scanner adapter and reviewed workstation hook wiring; no estate-wide conformance inferred", - "observed": "Inherited generated-only hook allowed a synthetic private-key commit. New pinned scanner adapter detects that fixture in complete changed staged blobs with real-scanner tests.", - "remaining": "Reviewed source is not activated in the configured hooks. Production repo denominator, actual automation coverage and historical/runtime findings remain unverified.", + "observed": "Inherited generated-only hook allowed a synthetic private-key commit. New pinned scanner adapter detects that fixture in complete changed staged blobs with real-scanner tests. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged.", + "remaining": "Production repository denominator, other workstation clones, independent automation coverage and historical/runtime findings remain unverified.", "owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03", "verification_evidence": [ - "secret-detection.json" + "secret-detection.json", + "secret-hook-activation.json" ], "assessed_at": "2026-10-12" } @@ -864,11 +865,12 @@ "closure_evidence": [], "assessment": { "scope": "Repo Manager scanner adapter and reviewed workstation hook wiring; no estate-wide conformance inferred", - "observed": "Verified local hook wiring in 21 review repositories plus Custodian; source adapter covers changed Git index blobs only.", - "remaining": "Native local activation, push/server/CI controls, logs/artifacts/runtime detection and independent boundary coverage remain unverified.", + "observed": "Verified local hook wiring in 21 review repositories plus Custodian; source adapter covers changed Git index blobs only. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged.", + "remaining": "Push/server/CI controls, logs/artifacts/runtime detection and independent boundary coverage remain unverified.", "owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03", "verification_evidence": [ - "secret-detection.json" + "secret-detection.json", + "secret-hook-activation.json" ], "assessed_at": "2026-10-12" } @@ -887,11 +889,12 @@ "closure_evidence": [], "assessment": { "scope": "Repo Manager scanner adapter and reviewed workstation hook wiring; no estate-wide conformance inferred", - "observed": "A disposable secret commit succeeds through the inherited hook and is rejected through the candidate scanner hook; git commit --only and partial/alternate-index cases are tested.", - "remaining": "Configured estate hooks have not been changed, and independent push prevention has not been demonstrated. A local hook alone remains bypassable.", + "observed": "A disposable secret commit succeeds through the inherited hook and is rejected through the candidate scanner hook; git commit --only and partial/alternate-index cases are tested. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged.", + "remaining": "Independent push prevention has not been demonstrated. A local hook alone remains bypassable; introduced-history and remote acceptance proof are required.", "owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03", "verification_evidence": [ - "secret-detection.json" + "secret-detection.json", + "secret-hook-activation.json" ], "assessed_at": "2026-10-12" } diff --git a/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation-record.json b/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation-record.json new file mode 100644 index 00000000..8913bc18 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation-record.json @@ -0,0 +1,25 @@ +{ + "proposal_id": "CUST80-G07-local-hooks-001", + "original_proposal_sha256": "bf1d984168478d4d79e77c3833690bb266e312d147877e4196e490070411e94c", + "executed_proposal_sha256": "bf1d984168478d4d79e77c3833690bb266e312d147877e4196e490070411e94c", + "recorded_at": "2026-10-12T15:47:11.086132+00:00", + "execution_permission": "Exact escalated installation command approved by the tool permission flow in this session; no new autopilot or production authority inferred.", + "verified_outcome": "22 native hook profiles accepted clean input and rejected synthetic secrets, generated-only changes and unavailable scanners; routing/other hooks/unreviewed checkout behavior preserved.", + "refinements_during_execution": [], + "recovery": "No rollback needed; scanner executable mode restored after deliberate failure probe. Exact prior-hook rollback remains available.", + "tests": "11 targeted activation/rollback/generated-guard tests passed; make generated-commit-hook idempotently preserved composed local hook.", + "promotion_scoring": "unscored; filesystem approval is not evidence of an agent runtime isolation grant", + "cost_eur": 0, + "risk_remaining": "Local hooks bypassable; remote/CI and response not yet accepted.", + "owner_commit": "21498306ecc9f8fb19f67ca3d0cdb38b76f984e4", + "owner_sync": { + "status": "applied", + "instance_role": "primary", + "instance_label": "railiance01", + "derived_commit": "21498306ecc9f8fb19f67ca3d0cdb38b76f984e4" + }, + "owner_task_readback": { + "id": "e27817e6-b298-581f-b99a-05ec459be749", + "status": "done" + } +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation.json b/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation.json new file mode 100644 index 00000000..2f94c4f2 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/secret-hook-activation.json @@ -0,0 +1,212 @@ +{ + "proposal_id": "CUST80-G07-local-hooks-001", + "proposal_sha256": "bf1d984168478d4d79e77c3833690bb266e312d147877e4196e490070411e94c", + "executor_sha256": "1935cc442026a36db8fecc7f58f779dc877a79b6b2e987f8ab074138230ff74f", + "result": "verified", + "cost_eur": 0, + "verification": { + "repositories": [ + { + "repo": "access-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "approval-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "audit-core", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "gate-house", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "glas-harness", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "key-cape", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "kings-guard", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "maturity-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "net-kingdom", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "ops-mason", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "ops-warden", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "policy-nexus", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "railiance-forge", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "railiance-platform", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "repo-manager", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "role-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "secrets-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "security-canon", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "tenant-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "the-custodian", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "user-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + }, + { + "repo": "zone-engine", + "clean_exit": 0, + "secret_exit": 1, + "generated_only_exit": 1, + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "scanner_failure_exit": 2, + "scanner_failure_reason": "scanner-file-refused" + } + ], + "unreviewed_checkout_preserved": true, + "indexes_and_objects": "temporary alternate stores only", + "scanner_mode_restored": true + } +} diff --git a/docs/netkingdom-security-blueprint.md b/docs/netkingdom-security-blueprint.md index 6a1dffa2..b5412c7c 100644 --- a/docs/netkingdom-security-blueprint.md +++ b/docs/netkingdom-security-blueprint.md @@ -150,9 +150,12 @@ only if its authority remains valid and unexposed. Repository prevention uses Repo Manager as a control adapter, with the external scanner owning detection rules and Forge owning independent push/CI enforcement. -The reviewed inherited hook currently enforces generated-only commits; it is not -a secret scanner. RMGR-WP-0021 adds pinned, content-silent staged-blob scanning -and separately tracks actual activation and response. A local hook is bypassable, +The reviewed shared hook and Custodian local hook now compose generated-only +commit rejection with pinned, content-silent staged-blob scanning for 22 exact +workstation checkout paths. Actual hooks passed clean, disposable-key rejection +and scanner-failure checks; other hooks/routing and unreviewed checkout behavior +were preserved. RMGR-WP-0021-T02 records local activation; independent forge/CI +enforcement and response remain open. [Activation receipt](evidence/2026-10-12-netkingdom-security/secret-hook-activation.json). A local hook is bypassable, so it cannot substitute for independent introduced-history checks or governed incident response. [Coverage evidence](evidence/2026-10-12-netkingdom-security/secret-detection.json). diff --git a/docs/netkingdom-security-gaps.md b/docs/netkingdom-security-gaps.md index 6572c220..021e8702 100644 --- a/docs/netkingdom-security-gaps.md +++ b/docs/netkingdom-security-gaps.md @@ -15,7 +15,7 @@ No whole-estate SEC-DSM conformance or maturity level is claimed. | G04 | Credential inventory is incomplete and its five metadata fields do not by themselves establish full SEC-DSM lifecycle coverage. Mason's dated survey found 49 paths; coverage is not current estate-wide discovery. | MASON-WP-0004-T03/T04; RPF-WP-0035; CUST-WP-0080-T03 | Current metadata-only discovery denominator; owners/consumers/scopes; rotation and revocation procedures; unknown entries remain findings | | G05 | Agent effective authority and credential isolation are unproven. Glas declares no ready profile; the Custodian record grants no autopilot authority. | CUST-WP-0073-T02/T04; GLAS-WP-0012 | Actual admitted agent identity, runtime read-denial and tool-effect restrictions, task expiry/cancel cleanup, no cross-task credential accumulation | | G06 | Native evidence delivery, source completeness and attribution remain incomplete. Independent archive custody is not source completeness. | AUDIT-WP-0013/0014/0015; RPF-WP-0035-T08; existing FLEX-WP-0031 and NK-WP-0035/0040 residual records | Admitted sender, durable outbox, native archive readback, heartbeat/reconciliation and failure recovery. Residual work requires explicit promotion | -| G07 | Inherited hooks in all 21 reviewed repositories enforce generated-only commits, not secret detection; a disposable private-key commit was accepted. New pinned staged-object scanner adapter passes synthetic refusal tests, but configured local hooks and independent forge/CI prevention remain unverified. | RMGR-WP-0021-T01 source done; T02 local activation, T03 forge/CI, T04 response; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure | +| G07 | The original generated-only hook accepted a disposable private-key commit. The pinned scanner is now enforced by actual configured hooks on 22 exact workstation checkouts; clean, synthetic-secret, generated-only and unavailable-scanner probes pass. Independent forge/CI prevention and response remain unverified. | RMGR-WP-0021-T01/T02 source and local activation done; T03 forge/CI, T04 response open; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure | | G08 | Legacy root export and generation-prefix disclosure reproduced and removed in source. Bootstrap now uses a registered public recipient, explicit temporary-key resume and an encrypted handoff bound to the bundle digest. Other custody callers and native recovery acceptance remain unverified. | NK-WP-0049 T01 repaired; T02 caller reconciliation; T03 native recovery. NK-WP-0045 covers only the non-secret root-grant record; CUST-WP-0073 retains isolation/incident work | Inspect actual bootstrap callers; stop secret output reaching model/logs; owner-reviewed safe recovery procedure and tests | | G09 | Rotation and revocation closure are incomplete: existing exposure rotation is founder-deferred, npm migration remains open, and five-field descriptions do not prove consumer adoption. | CUST-WP-0073-T05; SECRETS-WP-0006-T06; WARDEN-WP-0037-T03; RPF-WP-0035-T07 | For each relevant credential: new consumer accepted, old credential refused, copies reconciled; explicit DSM exception if still deferred | | G10 | Build-mode exceptions and close-down checklist do not uniformly contain DSM-EXC requirement, risk, compensating controls and review fields. | CUST-WP-0080-T03; existing security-close-down checklist and October 11 decisions | Requirement-level exception records with real approval references and revalidation; no invented approval or retroactive conformance | @@ -93,8 +93,13 @@ A synthetic private key committed successfully through the actual inherited hook Repo Manager `77fc4c9` supplies a pinned Gitleaks staged-object adapter that rejects a synthetic key without logging findings, checks partial/alternate indexes and `git commit --only`, and refuses missing/failed tools or unsupported inputs. -All 249 owner tests pass. The shared hook has not been replaced. RMGR-WP-0021 explicitly owns local -activation, independent forge/CI coverage and private response acceptance. +All 249 owner tests pass. Subsequent reviewed activation composed the shared and +Custodian local hooks with this scanner. All 22 exact checkouts passed actual-hook +clean/secret/generated-only/scanner-failure probes using temporary indexes and +objects. The executable mode, other hooks and routing were preserved; unreviewed +checkout behavior was unchanged. Eleven activation/rollback/guard tests passed. +RMGR-WP-0021-T02 is done; independent forge/CI coverage and private response +acceptance remain open. [Activation receipt](evidence/2026-10-12-netkingdom-security/secret-hook-activation.json). All four DSM-DETECT requirements now have incomplete assessments rather than unverified placeholders. [Receipt and exact hook inventory](evidence/2026-10-12-netkingdom-security/secret-detection.json). diff --git a/scripts/install_shared_generated_guard.py b/scripts/install_shared_generated_guard.py index dd106744..4f9812a7 100644 --- a/scripts/install_shared_generated_guard.py +++ b/scripts/install_shared_generated_guard.py @@ -6,12 +6,17 @@ The repo argument discovers the already-configured hook path; it is not a new fleet routing or authority configuration. """ import argparse +import hashlib import os from pathlib import Path import shlex import subprocess +# Reviewed composition retains this guard and adds exact-scope secret scanning. +# CUST-WP-0080 / RMGR-WP-0021; unknown hooks still require owner integration. +SCANNED_HOOK_SHA256 = "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd" + CHECKER = Path(__file__).with_name("check_generated_only_commit.py").resolve() @@ -30,7 +35,8 @@ def install(repo, execute=False): if target.is_symlink(): raise ValueError("refuse redirected existing pre-commit hook") if target.exists(): - if target.read_text() != content: + if (target.read_text() != content + and hashlib.sha256(target.read_bytes()).hexdigest() != SCANNED_HOOK_SHA256): raise ValueError("existing pre-commit hook differs; owner integration required") return {"repo": str(repo), "hook": str(target), "status": "installed", "changed": False} if execute: diff --git a/tests/test_secret_hook_activation.py b/tests/test_secret_hook_activation.py new file mode 100644 index 00000000..c592572b --- /dev/null +++ b/tests/test_secret_hook_activation.py @@ -0,0 +1,85 @@ +"""Exercise reviewed hook activation/rollback in disposable repositories only.""" +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / 'docs/changes/CUST-WP-0080/activate_secret_hooks.py' + + +def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() + + +@pytest.fixture +def proposal(tmp_path): + binary_source = Path('/tmp/netkingdom-secret-detection/gitleaks') + if not binary_source.exists(): pytest.skip('reviewed disposable scanner not installed') + repo = tmp_path / 'scoped'; repo.mkdir() + def git(*args): + return subprocess.check_output(['git', '-C', str(repo), *args], stderr=subprocess.DEVNULL) + git('init', '-q') + hooks = repo / '.git/hooks' + git('config', 'core.hooksPath', str(hooks)) + git('config', 'user.name', 'Synthetic Probe'); git('config', 'user.email', 'probe@example.test') + (repo / 'source.py').write_text('print("clean")\n') + git('add', 'source.py'); git('commit', '-qm', 'clean baseline') + before = hooks / 'pre-commit'; before.write_text('#!/bin/sh\nexit 0\n'); before.chmod(0o755) + backup = tmp_path / 'backup'; shutil.copyfile(before, backup) + binary = tmp_path / 'tools/gitleaks' + candidate = tmp_path / 'candidate' + candidate.write_text('#!/bin/sh\nset -eu\npython3 '+str(ROOT/'scripts/check_generated_only_commit.py')+' --staged\n' + 'case "$(git rev-parse --show-toplevel)" in\n'+str(repo)+')\n' + 'exec python3 /home/worsch/repo-manager/src/repo_manager/secret_scan.py --path . --binary ' + +str(binary)+'\n;;\nesac\n') + data = {'proposal_id':'disposable-activation-probe','scope_roots':[str(repo)], + 'binary':{'source':str(binary_source),'target':str(binary),'sha256':sha(binary_source)}, + 'hook_source':str(candidate),'hooks':[{'path':str(before),'before_sha256':sha(before), + 'before_mode':0o755,'backup':str(backup),'after_sha256':sha(candidate)}], + 'original_routes':[{'repo':repo.name,'effective_pre_commit':str(before)}], + 'protected_files':[], 'executor_sha256':sha(SCRIPT)} + path = tmp_path / 'proposal.json'; path.write_text(json.dumps(data)) + return path, data, tmp_path / 'receipt.json', repo, git('rev-parse','HEAD') + + +def invoke(proposal, *args): + path, _, receipt, _, _ = proposal + return subprocess.run([sys.executable, str(SCRIPT), '--proposal', str(path), '--receipt', str(receipt), *args], + capture_output=True, text=True, check=False, timeout=60) + + +def test_activation_native_probes_restore_tool_and_preserve_repository(proposal): + _, data, receipt, repo, head = proposal + before_index = (repo/'.git/index').read_bytes() + assert invoke(proposal, '--apply').returncode == 0 + observed = json.loads(receipt.read_text()) + assert observed['result'] == 'verified' + assert observed['verification']['repositories'][0]['scanner_failure_exit'] == 2 + assert observed['verification']['scanner_mode_restored'] + assert (repo/'.git/index').read_bytes() == before_index + assert subprocess.check_output(['git','-C',str(repo),'rev-parse','HEAD']) == head + assert invoke(proposal, '--rollback').returncode == 0 + assert sha(Path(data['hooks'][0]['path'])) == data['hooks'][0]['before_sha256'] + + +def test_unknown_hook_is_refused_before_install(proposal): + _, data, _, _, _ = proposal + Path(data['hooks'][0]['path']).write_text('unreviewed hook\n') + assert invoke(proposal, '--apply').returncode != 0 + assert not Path(data['binary']['target']).exists() + + +def test_failed_probe_rolls_back_exact_hook(proposal): + path, data, receipt, _, _ = proposal + candidate = Path(data['hook_source']); candidate.write_text('#!/bin/sh\nexit 0\n') + data['hooks'][0]['after_sha256'] = sha(candidate); path.write_text(json.dumps(data)) + assert invoke(proposal, '--apply').returncode != 0 + observed = json.loads(receipt.read_text()) + assert observed['result'] == 'unverified' + assert observed['rollback'] == 'prior hooks restored' + assert sha(Path(data['hooks'][0]['path'])) == data['hooks'][0]['before_sha256'] diff --git a/tests/test_shared_generated_guard.py b/tests/test_shared_generated_guard.py index f1f93219..f35ea9cd 100644 --- a/tests/test_shared_generated_guard.py +++ b/tests/test_shared_generated_guard.py @@ -55,6 +55,19 @@ def test_foreign_or_redirected_guard_is_never_replaced(tmp_path): assert private.read_text() == "retained\n" +def test_reviewed_scanner_composition_is_preserved(tmp_path): + repo, hooks = repository(tmp_path) + content = (SCRIPT.parents[1] / 'docs/changes/CUST-WP-0080/secret-hooks-pre-commit').read_bytes() + hook = hooks / 'pre-commit' + hook.write_bytes(content) + hook.chmod(0o755) + assert not guard.install(repo, True)['changed'] + assert hook.read_bytes() == content + hook.write_bytes(content + b'\n# unreviewed revision\n') + with pytest.raises(ValueError): + guard.install(repo, True) + + @pytest.mark.parametrize("existing_local", [False, True]) def test_make_install_recognizes_existing_shared_guard(tmp_path, existing_local): import shutil diff --git a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md index a0d5957e..bd3715e4 100644 --- a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md +++ b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md @@ -151,6 +151,18 @@ changed and no real credential finding was exposed. Four DSM-DETECT assessments are now explicitly incomplete (12 incomplete, 84 unverified, 20 references). Evidence: `secret-detection.json`; G07 stays open pending native enforcement. +2026-10-12 G07 native workstation activation: RMGR-WP-0021-T02 completed. +Exact proposal CUST80-G07-local-hooks-001 installed the pinned scanner at a +durable local path and composed the two known hooks, preserving routing and +other hooks. All 22 scoped checkouts passed actual configured-hook clean, +disposable-secret, generated-only and non-executable-scanner probes. Temporary +indexes/object stores preserved user source/staging/branches; executable mode was +restored, and an unreviewed checkout kept its prior behavior. Eleven targeted +activation/rollback/guard tests pass. Receipt: `secret-hook-activation.json`; +exact proposal, backups and rollback: `docs/changes/CUST-WP-0080/`. Independent +forge/CI enforcement, other clones/history and detection-to-response remain open. +No DSM requirement or whole G07 closure is inferred from this local acceptance. + ## Verify native closure against every requirement ```task