"""Reproduce AI Vanguard's public-advisory tally. Python 3, standard library only.

Run: python audit-firefox-advisory.py --output-dir ./advisory-audit
Optional --source PATH reads a previously saved advisory instead of fetching it.
Counts reporter credits, not attribution of a finding to a particular model.
"""
import argparse
import csv
import hashlib
import json
import urllib.request
from collections import Counter
from html.parser import HTMLParser
from pathlib import Path

SOURCE = 'https://www.mozilla.org/en-US/security/advisories/mfsa2026-118/'

class AdvisoryParser(HTMLParser):
    def __init__(self):
        super().__init__(); self.rows=[]; self.row=None; self.mode=None; self.text=[]; self.key=None
    def handle_starttag(self,tag,attrs):
        attrs=dict(attrs)
        if tag=='h4' and attrs.get('id','').startswith('CVE-'):
            if self.row: self.rows.append(self.row)
            self.row={'cve':attrs['id']}; self.key=None
        if self.row and tag in ('dt','dd'):
            self.mode=tag; self.text=[]
    def handle_data(self,data):
        if self.mode: self.text.append(data)
    def handle_endtag(self,tag):
        if tag==self.mode:
            text=' '.join(''.join(self.text).split())
            if tag=='dt': self.key=text
            elif self.key in ('Reporter','Impact'): self.row[self.key.lower()]=text
            self.mode=None; self.text=[]
    def result(self):
        return self.rows+([self.row] if self.row else [])

def analyze(raw):
    parser=AdvisoryParser(); parser.feed(raw.decode('utf-8')); rows=parser.result()
    if not rows or any(not {'cve','reporter','impact'} <= row.keys() for row in rows):
        raise ValueError('Advisory structure changed; inspect the source instead of using a partial tally.')
    if len({row['cve'] for row in rows}) != len(rows): raise ValueError('Duplicate CVE identifiers')
    groups=Counter(row['reporter'] for row in rows if 'OpenAI' in row['reporter'])
    return {'source':SOURCE,'source_sha256':hashlib.sha256(raw).hexdigest(),'total_unique_entries':len(rows),
            'openai_credited_entries':sum(groups.values()),'reporter_groups':dict(sorted(groups.items())),
            'impact_counts_for_openai_entries':dict(Counter(row['impact'] for row in rows if 'OpenAI' in row['reporter'])),
            'method':'One unique CVE heading per entry. Count the Reporter field when it contains OpenAI. Keep distinct reporter labels separate.',
            'limitations':'One advisory only. Reporter credits do not identify the model. This neither reproduces exploits nor validates the broader 81-finding claim.'}

def main():
    ap=argparse.ArgumentParser(description=__doc__); ap.add_argument('--source',type=Path); ap.add_argument('--output-dir',type=Path,default=Path('advisory-audit')); args=ap.parse_args()
    if args.source: raw=args.source.read_bytes()
    else:
        req=urllib.request.Request(SOURCE,headers={'User-Agent':'AI-Vanguard-AdvisoryAudit/1.0 (+https://aivanguard.tech/)'})
        with urllib.request.urlopen(req,timeout=30) as response: raw=response.read()
    result=analyze(raw); args.output_dir.mkdir(parents=True,exist_ok=True)
    (args.output_dir/'advisory-audit-method.json').write_text(json.dumps(result,indent=2),encoding='utf-8')
    with (args.output_dir/'gpt-6-cyber-advisory-tally.csv').open('w',encoding='utf-8',newline='') as f:
        writer=csv.writer(f); writer.writerow(['reporter_credit','entry_count','source'])
        for reporter,count in result['reporter_groups'].items(): writer.writerow([reporter,count,SOURCE])
    print(json.dumps(result,indent=2))

if __name__=='__main__': main()
