204 lines
10 KiB
Python
204 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Reviewed-fixture-only probes of two actual hooks; never opens a real index.
|
|
|
|
No hook/scanner/source/config or Git commit/ref mutation, network/provider calls,
|
|
real credential reads, or unavailable-scanner injection. The only persistent
|
|
write is the requested fixed-metadata receipt in this review packet.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import types
|
|
|
|
SOURCE = Path('/home/worsch/repo-manager/src/repo_manager/secret_scan.py')
|
|
SOURCE_SHA = 'e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400'
|
|
BINARY = Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks')
|
|
BINARY_SHA = '88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509'
|
|
HOOK_SHA = 'd50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd'
|
|
ROOTS = (
|
|
(Path('/home/worsch/repo-manager'), Path('/home/worsch/repo-manager/.githooks/pre-commit')),
|
|
(Path('/home/worsch/the-custodian'), Path('/home/worsch/the-custodian/.git/hooks/pre-commit')),
|
|
)
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
|
|
class Refused(Exception):
|
|
pass
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def info(path):
|
|
try:
|
|
item = path.lstat()
|
|
except FileNotFoundError:
|
|
return None
|
|
return (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns, item.st_mode)
|
|
|
|
|
|
def fixed_env():
|
|
return {'PATH': '/usr/bin:/bin', 'HOME': '/home/worsch', 'LANG': 'C',
|
|
'GIT_CONFIG_NOSYSTEM': '1', 'GIT_NO_REPLACE_OBJECTS': '1'}
|
|
|
|
|
|
def run(argv, root, env, *, data=None, adapter=None):
|
|
kwargs = {}
|
|
if adapter is not None:
|
|
kwargs['preexec_fn'] = functools.partial(adapter.child_limits, 16 * 1024**2, os.getpid())
|
|
result = subprocess.run(argv, cwd=root, env=env, input=data, stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL, timeout=20, check=False, **kwargs)
|
|
if result.returncode:
|
|
raise Refused('fixture-command-failed')
|
|
return result.stdout
|
|
|
|
|
|
def git(root, env, *args, data=None, adapter=None):
|
|
return run(['/usr/bin/git', '-C', str(root), *args], root, env, data=data, adapter=adapter)
|
|
|
|
|
|
def precheck():
|
|
if digest(SOURCE) != SOURCE_SHA or digest(BINARY) != BINARY_SHA:
|
|
raise Refused('source-or-scanner-drift')
|
|
binary_info = BINARY.lstat()
|
|
if (not stat.S_ISREG(binary_info.st_mode) or binary_info.st_mode & 0o022
|
|
or not binary_info.st_mode & 0o111 or binary_info.st_uid not in (0, os.getuid())):
|
|
raise Refused('scanner-mode-refused')
|
|
records = []
|
|
for root, hook in ROOTS:
|
|
env = fixed_env()
|
|
raw = git(root, env, 'rev-parse', '--git-path', 'hooks/pre-commit').decode().strip()
|
|
routed = Path(raw)
|
|
if not routed.is_absolute():
|
|
routed = root / routed
|
|
if routed.resolve() != hook.resolve() or digest(hook) != HOOK_SHA:
|
|
raise Refused('hook-routing-drift')
|
|
raw_index = git(root, env, 'rev-parse', '--git-path', 'index').decode().strip()
|
|
index = Path(raw_index)
|
|
if not index.is_absolute():
|
|
index = root / index
|
|
objects = git(root, env, 'rev-parse', '--git-path', 'objects').decode().strip()
|
|
if not Path(objects).is_absolute():
|
|
objects = str(root / objects)
|
|
records.append({'root': root, 'hook': hook, 'index': index,
|
|
'index_stat': info(index), 'head': git(root, env, 'rev-parse', 'HEAD').strip(),
|
|
'objects': objects})
|
|
return records
|
|
|
|
|
|
def main():
|
|
receipt = {'status': 'unverified', 'scope': 'actual configured shared and Custodian hooks; generated alternate-index fixtures only',
|
|
'source_sha256': SOURCE_SHA, 'scanner_sha256': BINARY_SHA, 'hook_sha256': HOOK_SHA,
|
|
'provider_calls': 0, 'real_credentials_read': False, 'real_index_opened': False,
|
|
'actual_hook_unavailable_scanner': 'pending; no host scanner mutation performed'}
|
|
fixture = None
|
|
observations = []
|
|
try:
|
|
records = precheck()
|
|
# Execute only the hash-bound module bytes already reviewed. This module
|
|
# provides the reviewed tmpfs/watchdog/child-death lifecycle primitives.
|
|
source_bytes = SOURCE.read_bytes()
|
|
if hashlib.sha256(source_bytes).hexdigest() != SOURCE_SHA:
|
|
raise Refused('source-drift')
|
|
adapter = types.ModuleType('reviewed_scanner_probe_storage')
|
|
exec(compile(source_bytes, str(SOURCE), 'exec'), adapter.__dict__)
|
|
prior_storage = set(Path('/dev/shm').glob('rmgr-secret-scan-*'))
|
|
with adapter.private_storage() as (fixture, watcher):
|
|
env = fixed_env()
|
|
env['GIT_CONFIG_GLOBAL'] = '/dev/null'
|
|
# Temporary-index reads must not invoke a configured fsmonitor or
|
|
# create split-index auxiliaries in the real repository directory.
|
|
overrides = [('core.fsmonitor', 'false'), ('core.splitIndex', 'false'),
|
|
('core.sparseCheckout', 'false'), ('core.untrackedCache', 'false')]
|
|
env['GIT_CONFIG_COUNT'] = str(len(overrides))
|
|
for number, (name, value) in enumerate(overrides):
|
|
env[f'GIT_CONFIG_KEY_{number}'] = name
|
|
env[f'GIT_CONFIG_VALUE_{number}'] = value
|
|
key_path = fixture / 'generated-fixture-key'
|
|
run(['/usr/bin/ssh-keygen', '-q', '-t', 'ed25519', '-N', '',
|
|
'-C', 'public-generated-hook-probe', '-f', str(key_path)], fixture, env,
|
|
adapter=adapter)
|
|
key = key_path.read_bytes()
|
|
for number, record in enumerate(records):
|
|
root = record['root']; hook = record['hook']
|
|
local = fixture / str(number); local.mkdir(mode=0o700)
|
|
objects = local / 'objects'; objects.mkdir(mode=0o700)
|
|
probe_env = dict(env, GIT_INDEX_FILE=str(local / 'index'),
|
|
GIT_OBJECT_DIRECTORY=str(objects),
|
|
GIT_ALTERNATE_OBJECT_DIRECTORIES=record['objects'])
|
|
name = '__rmgr_scanner_batch_generated_fixture__.txt'
|
|
if git(root, probe_env, 'ls-tree', '--name-only', 'HEAD', '--', name, adapter=adapter):
|
|
raise Refused('fixture-name-collision')
|
|
row = {'repo': root.name, 'cases': []}
|
|
for case, payload, expected, expected_exit in [
|
|
('clean', b'public generated clean hook probe\n', 'clean', 0),
|
|
('generated-private-key', key, 'blocked', 1),
|
|
]:
|
|
if watcher.poll() is not None:
|
|
raise Refused('fixture-watchdog-lost')
|
|
# HEAD metadata seeds only the temporary index. No worktree
|
|
# files, existing blob contents, real index or refs are read.
|
|
git(root, probe_env, 'read-tree', 'HEAD', adapter=adapter)
|
|
oid = git(root, probe_env, 'hash-object', '-w', '--stdin', data=payload,
|
|
adapter=adapter).decode().strip()
|
|
git(root, probe_env, 'update-index', '--add', '--cacheinfo',
|
|
'100644,' + oid + ',' + name, adapter=adapter)
|
|
changed = git(root, probe_env, 'diff', '--cached', '--name-only', '-z',
|
|
'--no-renames', '--no-ext-diff', '--no-textconv', adapter=adapter)
|
|
if changed != name.encode() + b'\0':
|
|
raise Refused('fixture-changed-object-scope-refused')
|
|
result = subprocess.run([str(hook)], cwd=root, env=probe_env,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30,
|
|
check=False, preexec_fn=functools.partial(adapter.child_limits,
|
|
64 * 1024**2, os.getpid()))
|
|
if result.stderr or key.strip() in result.stdout:
|
|
raise Refused('hook-output-refused')
|
|
body = json.loads(result.stdout)
|
|
if set(body) != {'status', 'scope', 'scanner_version', 'scanner_sha256',
|
|
'scanned_blobs', 'blocked_blob_ids'}:
|
|
raise Refused('hook-summary-schema-refused')
|
|
if (result.returncode != expected_exit or body['status'] != expected
|
|
or body['scope'] != 'changed-staged-blobs'
|
|
or body['scanner_version'] != '8.30.1'
|
|
or body['scanner_sha256'] != BINARY_SHA or body['scanned_blobs'] != 1
|
|
or body['blocked_blob_ids'] != ([oid] if expected == 'blocked' else [])):
|
|
raise Refused('hook-result-refused')
|
|
row['cases'].append({'case': case, 'status': expected,
|
|
'exit_code': expected_exit, 'scanned_blobs': 1})
|
|
observations.append(row)
|
|
# Watcher normal completion, source children and fixture directory cleanup
|
|
# have all completed before these postconditions are evaluated.
|
|
if fixture.exists() or set(Path('/dev/shm').glob('rmgr-secret-scan-*')) - prior_storage:
|
|
raise Refused('fixture-cleanup-unverified')
|
|
again = precheck()
|
|
for before, after in zip(records, again):
|
|
if before['index_stat'] != after['index_stat'] or before['head'] != after['head']:
|
|
raise Refused('real-repository-state-changed')
|
|
receipt.update(status='verified', cases=observations, fixture_removed=True,
|
|
new_scan_storage_absent=True, real_index_stat_unchanged=True,
|
|
real_head_unchanged=True, routing_and_artifacts_unchanged=True)
|
|
except Refused as error:
|
|
receipt['reason'] = error.args[0]
|
|
except (OSError, ValueError, subprocess.SubprocessError, adapter.Unverified if 'adapter' in locals() else OSError):
|
|
receipt['reason'] = 'probe-unavailable'
|
|
receipt['fixture_removed'] = fixture is None or not fixture.exists()
|
|
receipt['driver_sha256'] = digest(Path(__file__))
|
|
(HERE / 'configured-hook-results.json').write_text(json.dumps(receipt, indent=2) + '\n')
|
|
print(json.dumps({'status': receipt['status'], 'fixture_removed': receipt['fixture_removed'],
|
|
'cases': sum(len(row['cases']) for row in observations)}))
|
|
return 0 if receipt['status'] == 'verified' else 2
|
|
|
|
|
|
if __name__ == '__main__':
|
|
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
signal.signal(signum, lambda *_: (_ for _ in ()).throw(Refused('probe-interrupted')))
|
|
raise SystemExit(main())
|