security: verify forge scanner expansion across 166 repositories
This commit is contained in:
parent
9af8689327
commit
a3b56d689f
14 changed files with 589 additions and 26 deletions
69
docs/changes/CUST-WP-0080/probe_forge_api_access.py
Normal file
69
docs/changes/CUST-WP-0080/probe_forge_api_access.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent read-only API admission through the existing protected credential route."""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
URL = 'https://forgejo.coulomb.social/api/v1/user'
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
|
||||
def inner(receipt):
|
||||
result={'scope':'GET /api/v1/user only; no repository, identity or credential changes',
|
||||
'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'result':'unverified'}
|
||||
token=os.environ.get('API_TOKEN','')
|
||||
if not token:
|
||||
result['reason']='protected-injection-absent'
|
||||
else:
|
||||
try:
|
||||
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
|
||||
request=urllib.request.Request(URL,headers={'Authorization':'token '+token,'Accept':'application/json'})
|
||||
with opener.open(request,timeout=15) as response:
|
||||
result['http_status']=response.status
|
||||
body=json.loads(response.read(65536))
|
||||
result['expected_caller']=body.get('login')=='tegwick'
|
||||
result['caller_id']=body.get('id') if isinstance(body.get('id'),int) else None
|
||||
result['site_admin']=body.get('is_admin') is True
|
||||
result['result']='authenticated' if result['http_status']==200 and result['expected_caller'] else 'unverified'
|
||||
except urllib.error.HTTPError as error:
|
||||
result.update(http_status=error.code,reason='http-refused')
|
||||
except Exception:
|
||||
result['reason']='request-unverified'
|
||||
receipt.write_text(json.dumps(result,indent=2)+'\n')
|
||||
return 0 if result['result']=='authenticated' else 1
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--receipt',type=Path,required=True)
|
||||
parser.add_argument('--inner',action='store_true')
|
||||
args=parser.parse_args()
|
||||
if args.inner:return inner(args.receipt)
|
||||
if args.receipt.exists():raise SystemExit('Receipt already exists; retain the prior attempt.')
|
||||
env=dict(os.environ,WARDEN_AGENT_ID='custodian-codex')
|
||||
result=subprocess.run(['warden','access','forgejo-admin-api-token','--field','API_TOKEN','--exec','--',
|
||||
sys.executable,str(Path(__file__).resolve()),'--inner','--receipt',str(args.receipt.resolve())],
|
||||
capture_output=True,env=env,timeout=90)
|
||||
if args.receipt.exists():record=json.loads(args.receipt.read_text())
|
||||
else:record={'result':'route-unavailable','scope':'Protected read-only API admission; child receipt absent'}
|
||||
record.update(route_exit_code=result.returncode,executor_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
token_returned_to_agent=False,raw_child_diagnostics_retained=False)
|
||||
args.receipt.write_text(json.dumps(record,indent=2)+'\n')
|
||||
print(json.dumps(record))
|
||||
return 0 if record['result']=='authenticated' else 1
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
try:sys.exit(main())
|
||||
except Exception:sys.exit('Protected API admission incomplete; raw diagnostics suppressed.')
|
||||
67
docs/changes/CUST-WP-0080/verify_forge_expansion.py
Normal file
67
docs/changes/CUST-WP-0080/verify_forge_expansion.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Independent read-only final inventory and cleanup audit for the exact cohort."""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE=Path(__file__).parent
|
||||
s=importlib.util.spec_from_file_location('expand',HERE/'expand_forge_profiles.py')
|
||||
m=importlib.util.module_from_spec(s)
|
||||
s.loader.exec_module(m)
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--inventory',type=Path,required=True)
|
||||
parser.add_argument('--fresh-inventory',type=Path,required=True)
|
||||
parser.add_argument('--inventory-receipt',type=Path,required=True)
|
||||
parser.add_argument('--receipt',type=Path,required=True)
|
||||
args=parser.parse_args()
|
||||
p=json.loads((HERE/'forge-expansion-proposal.json').read_text())
|
||||
activation=Path('docs/evidence/2026-10-13-netkingdom-security/forge-expansion-activation.json')
|
||||
a=json.loads(activation.read_text())
|
||||
assert a['result']=='verified' and a['ownership_markers_removed'] and a['vetoes_removed'],'activation-not-complete'
|
||||
assert a['proposal_sha256']==hashlib.sha256((HERE/'forge-expansion-proposal.json').read_bytes()).hexdigest()
|
||||
assert len(a['verified_ids'])==len(set(a['verified_ids']))==p['target_count']
|
||||
assert set(a['verified_ids'])==set(a['attempted_ids'])==set(p['target_ids'])
|
||||
assert hashlib.sha256(args.inventory.read_bytes()).hexdigest()==p['inventory_sha256']
|
||||
old=json.loads(args.inventory.read_text());baseline={r['id'] for r in old['repositories'] if r['profile']=='admitted'}
|
||||
process=subprocess.run([sys.executable,str(HERE/'inventory_forge_coverage.py'),
|
||||
'--private-output',str(args.fresh_inventory),'--receipt',str(args.inventory_receipt)],capture_output=True,timeout=180)
|
||||
if process.returncode:raise ValueError('fresh-inventory-incomplete')
|
||||
fresh=json.loads(args.fresh_inventory.read_text());by_id={r['id']:r for r in fresh['repositories']}
|
||||
expected=baseline|set(p['target_ids'])
|
||||
assert all(by_id[i]['profile']=='admitted' for i in expected),'profile-not-preserved'
|
||||
assert not any(r['profile']=='unknown' for r in by_id.values()),'unknown-hook-profile'
|
||||
original=json.loads((HERE/'receive-hook-proposal.json').read_text())
|
||||
pod=m.t.target_pod(m.t.deployment(),original)
|
||||
assert pod['pod_uid']==fresh['pod_uid'],'pod-changed-during-final-audit'
|
||||
commands=['set -euo pipefail']
|
||||
for i in p['target_ids']:
|
||||
path=m.repo_path(by_id[i])
|
||||
commands += ['repo='+shlex.quote(path),
|
||||
'test ! -e "$repo/hooks/pre-receive.d/'+p['veto_name']+'"',
|
||||
'test ! -e "$repo/.custodian-expand-'+original['hook_name']+'"',
|
||||
'test ! -e "$repo/.custodian-expand-'+p['veto_name']+'"',
|
||||
'test -z "$(git --git-dir="$repo" for-each-ref --format=\'%(refname)\' '+shlex.quote(p['probe_ref'])+')"']
|
||||
m.t.native.execute(pod,'bash','-s',payload=('\n'.join(commands)+'\n').encode(),timeout=180)
|
||||
summary=json.loads(args.inventory_receipt.read_text())
|
||||
receipt={'result':'verified','observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'proposal_id':p['proposal_id'],'activation_receipt_sha256':hashlib.sha256(activation.read_bytes()).hexdigest(),
|
||||
'baseline_profiles_preserved':len(baseline),'expanded_profiles_verified':len(p['target_ids']),
|
||||
'current_profiles':summary['profiles'],'current_repositories':summary['repositories'],
|
||||
'no_probe_refs_or_vetoes_or_ownership_markers':True,
|
||||
'blocked_synthetic_commits_absent':'Verified separately by every native SSH probe in the activation receipt.',
|
||||
'remaining_builtin_only_ids':[r['id'] for r in by_id.values() if r['profile']=='builtin-only'],
|
||||
'api_route':'Unverified: protected route did not hand off; current capabilities check denied; attended login required.',
|
||||
'scope':'Exact native SSH scanner verdicts under ref-specific vetoes plus fresh hook/directory/custody and cleanup audit. Ordinary source acceptance remains separate; no full DSM conformance claimed.'}
|
||||
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps(receipt))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue