#!/usr/bin/env python3
"""Fail closed on incomplete scans; emit one overall result and an HTML summary."""
import html
import hashlib
import json
from pathlib import Path
import sys
r=Path(sys.argv[1] if len(sys.argv)>1 else '/security')
a=Path(sys.argv[2] if len(sys.argv)>2 else '/artifacts')
rows=[]; errors=[]
def read(path):
    try: return json.loads((r/path).read_text())
    except (OSError, ValueError) as e:
        errors.append(f'{path}: missing or invalid report ({type(e).__name__})'); return None
for name in ['syft','grype','trivy']:
    try: code=int((r/f'status/{name}.exit').read_text())
    except (OSError,ValueError): code=None
    rows.append({'tool':name,'exit_code':code,'status':'passed' if code==0 else 'blocked'})
sbom=read('sbom/sbom.cdx.json'); syft=read('sbom/sbom.syft.json')
grype=read('grype/grype.json'); trivy=read('trivy/trivy-image.json')
if sbom is not None and sbom.get('bomFormat')!='CycloneDX': errors.append('Invalid CycloneDX SBOM')
if syft is not None and not isinstance(syft.get('artifacts'),list): errors.append('Invalid Syft SBOM')
if grype is not None and not isinstance(grype.get('matches'),list): errors.append('Invalid Grype report')
if trivy is not None and trivy.get('SchemaVersion')!=2: errors.append('Invalid Trivy report')
gcount=sum(m.get('vulnerability',{}).get('severity') in ['High','Critical'] for m in (grype or {}).get('matches',[]))
tcount=sum(v.get('Severity') in ['HIGH','CRITICAL'] for result in (trivy or {}).get('Results',[]) for v in result.get('Vulnerabilities',[]))
blocked=bool(errors or gcount or tcount or any(x['exit_code']!=0 for x in rows))
try: build=json.loads((a/'build.json').read_text())
except (OSError, ValueError): build={}; errors.append('Missing build identity'); blocked=True
try:
    h=hashlib.sha256()
    with (a/'app.tar').open('rb') as src:
        for chunk in iter(lambda: src.read(1024*1024),b''): h.update(chunk)
    if h.hexdigest()!=build.get('sha256'): errors.append('Candidate image archive changed after build'); blocked=True
except OSError: errors.append('Candidate image archive missing'); blocked=True
summary={'status':'FAIL' if blocked else 'PASS','build':build,'tools':rows,
 'components':len((sbom or {}).get('components',[])),
 'high_critical_matches':{'grype':gcount,'trivy':tcount},'errors':errors,
 'note':'Counts are per scanner, not deduplicated. Zero components is not proof of security.'}
r.mkdir(exist_ok=True)
(r/'summary.json').write_text(json.dumps(summary,indent=2)+'\n')
(r/'summary.html').write_text('<!doctype html><meta charset="utf-8"><title>my-app security</title><h1>my-app: '+summary['status']+'</h1><pre>'+html.escape(json.dumps(summary,indent=2))+'</pre>')
print(json.dumps(summary,indent=2)); print('SECURITY GATE: '+summary['status'])
sys.exit(1 if blocked else 0)
