Narrow scanner preflight failure to repository-hook inventory
This commit is contained in:
parent
a2e57251ac
commit
6696366f27
8 changed files with 214 additions and 5 deletions
|
|
@ -19,3 +19,5 @@ The staged scanner in repo-manager and live receiver have not been altered by th
|
|||
## Current execution outcome, 2026-11-02
|
||||
|
||||
The founder ran the exact revision-2 launcher. It failed during preflight with a suppressed Kubernetes command error, before staging or native mutation (`native_mutation: false`, no events). The exact failed read is not yet identified. Failure evidence is preserved in `attempt-r2-preflight/`; the reviewed packet is unchanged. A separate read-only diagnostic labels each check and outputs fixed failure categories; three tests passed, including raw-diagnostic suppression and receipt preservation. Founder invocation: `bash /tmp/forge-scanner-diagnose.sh`. Do not retry the old apply while this failure is unresolved; its existing receipt prevents an unreviewed retry.
|
||||
|
||||
The first diagnostic was returned: node/deployment/pod identity, readiness, old public hook and artifact guard all passed; existing repository hook inventory failed with remote exit1. Its selected receipt is preserved in `attempt-r2-preflight/diagnostic-receipt.json`. The pending request is now `bash /tmp/forge-scanner-inventory-diagnose.sh` to identify the specific filesystem condition without reading out hook contents. Five diagnostic tests pass, including real disposable filesystem cases.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"read_only": true,
|
||||
"replayed_packet_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237",
|
||||
"steps": [
|
||||
{
|
||||
"step": "node-identity-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "deployment-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "replicaset-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "pod-list-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "deployment-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "initial-pod-uid-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "old-public-hook-read",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "installed-artifact-guard",
|
||||
"returncode": 0,
|
||||
"category": "passed"
|
||||
},
|
||||
{
|
||||
"step": "existing-repository-hook-inventory",
|
||||
"returncode": 1,
|
||||
"category": "remote-check-nonzero",
|
||||
"remote_exit_code": 1
|
||||
}
|
||||
],
|
||||
"native_mutation": false,
|
||||
"original_failed_receipt_preserved": true,
|
||||
"result": "read-only-preflight-refused",
|
||||
"reason": "read-only-step-failed:existing-repository-hook-inventory"
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"""Founder-run read-only explanation of inventory conditions; no hook contents."""
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ORIGINAL = HERE.parent / 'native-r2'
|
||||
EXPECTED = 'd22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237'
|
||||
CONDITIONS = {'directory-missing', 'symlink', 'hook-symlink-present', 'symlink-query-failed',
|
||||
'dispatcher-missing', 'dispatcher-not-executable', 'scanner-fragment-missing',
|
||||
'hook-metadata-command-failed', 'repository-limit', 'no-repositories', 'wrong-uid'}
|
||||
SCRIPT = r'''set +vx
|
||||
set -u
|
||||
export PATH=/usr/bin:/bin
|
||||
issue() { printf 'ISSUE\t%s\t%s\n' "$1" "$2"; }
|
||||
check_directory() {
|
||||
test -d "$1" || issue directory-missing "$1"
|
||||
test ! -L "$1" || issue symlink "$1"
|
||||
}
|
||||
test "$(id -u)" = 1000 || issue wrong-uid /data
|
||||
for parent in /data /data/git /data/git/gitea-repositories; do check_directory "$parent"; done
|
||||
count=0
|
||||
for repo in /data/git/gitea-repositories/*/*.git; do
|
||||
test -d "$repo" || continue
|
||||
count=$((count+1))
|
||||
if test "$count" -gt 500; then issue repository-limit /data/git/gitea-repositories; break; fi
|
||||
for parent in "${repo%/*}" "$repo" "$repo/hooks" "$repo/hooks/pre-receive.d"; do check_directory "$parent"; done
|
||||
links=$(find "$repo/hooks" -type l -print -quit 2>/dev/null)
|
||||
code=$?
|
||||
if test "$code" != 0; then issue symlink-query-failed "$repo/hooks"
|
||||
elif test -n "$links"; then issue hook-symlink-present "$repo/hooks"; fi
|
||||
test -f "$repo/hooks/pre-receive" || issue dispatcher-missing "$repo/hooks/pre-receive"
|
||||
test -x "$repo/hooks/pre-receive" || issue dispatcher-not-executable "$repo/hooks/pre-receive"
|
||||
test -f "$repo/hooks/pre-receive.d/10-custodian-secret-scan" || issue scanner-fragment-missing "$repo/hooks/pre-receive.d/10-custodian-secret-scan"
|
||||
if ! find "$repo/hooks" -type f ! -name '*.sample' -exec sh -c '
|
||||
for file do
|
||||
stat -c %a "$file" >/dev/null || exit 1
|
||||
stat -c %u "$file" >/dev/null || exit 1
|
||||
stat -c %g "$file" >/dev/null || exit 1
|
||||
sha256sum "$file" >/dev/null || exit 1
|
||||
done
|
||||
' sh {} + >/dev/null 2>&1; then issue hook-metadata-command-failed "$repo/hooks"; fi
|
||||
done
|
||||
test "$count" -gt 0 || issue no-repositories /data/git/gitea-repositories
|
||||
printf 'COUNT\t%s\n' "$count"
|
||||
'''
|
||||
|
||||
|
||||
def parse(raw):
|
||||
issues = []; count = None
|
||||
for line in raw.decode().splitlines():
|
||||
parts = line.split('\t')
|
||||
if len(parts) == 2 and parts[0] == 'COUNT' and parts[1].isdigit():
|
||||
if count is not None: raise RuntimeError('duplicate-counter')
|
||||
count = int(parts[1]); continue
|
||||
if len(parts) != 3 or parts[0] != 'ISSUE' or parts[1] not in CONDITIONS:
|
||||
raise RuntimeError('unexpected-diagnostic-output; raw withheld')
|
||||
path = parts[2]
|
||||
if not re.fullmatch(r'/data(?:/[a-zA-Z0-9_.-]+)*', path) or '..' in path.split('/'):
|
||||
path = 'unsafe-path-withheld'
|
||||
issues.append({'condition': parts[1], 'path': path})
|
||||
if count is None: raise RuntimeError('missing-counter')
|
||||
return {'read_only': True, 'repositories_examined': count, 'issues': issues, 'native_mutation': False}
|
||||
|
||||
|
||||
def main():
|
||||
spec = importlib.util.spec_from_file_location('reviewed_r2_inventory', ORIGINAL / 'rollout.py')
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
||||
if m.verify_packet() != EXPECTED: raise RuntimeError('reviewed-packet-drift')
|
||||
c = json.loads((ORIGINAL / 'inputs.json').read_bytes())
|
||||
def k(*args):
|
||||
q = subprocess.run(['kubectl', *args], capture_output=True, timeout=90)
|
||||
if q.returncode: raise RuntimeError('read-only-command-failed; raw diagnostics withheld')
|
||||
return q.stdout
|
||||
for resource, name, expected in [('deployment', c['deployment_name'], c['deployment_uid']), ('pod', c['pod'], c['pod_uid'])]:
|
||||
if k('-n', 'forgejo', 'get', resource, name, '-o', 'jsonpath={.metadata.uid}').decode() != expected:
|
||||
raise RuntimeError('target-identity-drift')
|
||||
result = parse(k('-n', 'forgejo', 'exec', '-i', c['pod'], '-c', c['container'], '--', 'sh', '-c', SCRIPT))
|
||||
result['replayed_packet_sha256'] = EXPECTED
|
||||
out = Path('/tmp/forge-scanner-inventory-diagnostic-receipt.json')
|
||||
out.write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps(result, indent=2)); print('receipt:', str(out))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try: main()
|
||||
except Exception as exc:
|
||||
print(json.dumps({'result': 'read-only-diagnostic-refused', 'reason': str(exc) if isinstance(exc, RuntimeError) else type(exc).__name__}))
|
||||
raise SystemExit(1)
|
||||
|
|
@ -13,5 +13,19 @@
|
|||
"diagnostic_sha256": "301919aa82b9e9ae5f1a2d55175d2e988ba4c4ac955598651d7f25ba71ddc21c",
|
||||
"tests": "3 passed; failure label/redaction, command restrictions and preservation of original receipt",
|
||||
"promotion_evidence": false,
|
||||
"score": "unscored; no signed approval or separate approval evidence backfilled"
|
||||
"score": "unscored; no signed approval or separate approval evidence backfilled",
|
||||
"followup_diagnostic": {
|
||||
"receipt_ref": "diagnostic-receipt.json",
|
||||
"passed_checks": [
|
||||
"node/deployment/pod identities",
|
||||
"readiness",
|
||||
"old public hook",
|
||||
"installed artifact guard"
|
||||
],
|
||||
"failed_check": "existing-repository-hook-inventory",
|
||||
"category": "remote-check-nonzero",
|
||||
"remote_exit_code": 1,
|
||||
"next_check": "inventory_conditions.py; read-only filesystem condition report",
|
||||
"diagnostic_tests": "5 passed in total, including actual disposable filesystem fixtures"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
"""Exercise actual read-only shell against disposable filesystem fixtures."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location('inventory_conditions', HERE / 'inventory_conditions.py')
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_clean_missing_fragment_nonexecutable_dispatcher_and_symlink(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
data = Path(temp) / 'data'
|
||||
hooks = data / 'git/gitea-repositories/coulomb/example.git/hooks'
|
||||
fragments = hooks / 'pre-receive.d'; fragments.mkdir(parents=True)
|
||||
dispatcher = hooks / 'pre-receive'; dispatcher.write_text('#!/bin/sh\nexit 0\n'); dispatcher.chmod(0o755)
|
||||
fragment = fragments / '10-custodian-secret-scan'; fragment.write_text('#!/bin/sh\nexit 0\n'); fragment.chmod(0o775)
|
||||
script = m.SCRIPT.replace('/data', str(data))
|
||||
def issues():
|
||||
q = subprocess.run(['sh', '-c', script], capture_output=True, timeout=10)
|
||||
self.assertEqual(q.returncode, 0)
|
||||
result = m.parse(q.stdout)
|
||||
self.assertEqual(result['repositories_examined'], 1)
|
||||
return {x['condition'] for x in result['issues']} - {'wrong-uid'}
|
||||
self.assertEqual(issues(), set())
|
||||
fragment.unlink(); self.assertEqual(issues(), {'scanner-fragment-missing'})
|
||||
fragment.write_text('#!/bin/sh\nexit 0\n'); fragment.chmod(0o775)
|
||||
dispatcher.chmod(0o644); self.assertEqual(issues(), {'dispatcher-not-executable'})
|
||||
dispatcher.chmod(0o755)
|
||||
original = hooks / 'saved.d'; fragments.rename(original); fragments.symlink_to(original, target_is_directory=True)
|
||||
self.assertEqual(issues(), {'symlink', 'hook-symlink-present'})
|
||||
self.assertTrue((original / fragment.name).exists())
|
||||
|
||||
def test_parser_rejects_unframed_data_and_hides_unsafe_paths(self):
|
||||
with self.assertRaisesRegex(RuntimeError, '^unexpected-diagnostic-output; raw withheld$'):
|
||||
m.parse(b'PRIVATE_SENTINEL\n')
|
||||
result = m.parse(b'ISSUE\tsymlink\t/elsewhere/PRIVATE_SENTINEL\nCOUNT\t1\n')
|
||||
self.assertEqual(result['issues'][0]['path'], 'unsafe-path-withheld')
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue