#!/usr/bin/env python3 """Mirror public Oracle security advisories (HTML and linked CSAF JSON).""" import argparse import hashlib from html.parser import HTMLParser import json import os from pathlib import Path import re import time from urllib.parse import urljoin, urlparse from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError BASE = 'https://www.oracle.com' INDEX = BASE + '/security-alerts/' ARCHIVE = BASE + '/security-alerts/cpuarchive.html' UA = 'vuln-oracle-mirror/1.0 (+security data mirror; contact site administrator)' YEAR = re.compile(r'\b(20\d{2})\b') class Links(HTMLParser): def __init__(self): super().__init__() self.links=[]; self.href=None; self.parts=[] def handle_starttag(self, tag, attrs): if tag=='a': self.href=dict(attrs).get('href'); self.parts=[] def handle_data(self, data): if self.href is not None: self.parts.append(data) def handle_endtag(self,tag): if tag=='a' and self.href is not None: self.links.append((self.href,' '.join(''.join(self.parts).split()))) self.href=None; self.parts=[] def get(url, tries=4): last=None for i in range(tries): try: with urlopen(Request(url, headers={'User-Agent':UA,'Accept':'text/html,application/json,*/*'}), timeout=90) as response: body=response.read(30_000_001) if len(body)>30_000_000: raise ValueError('Document too large') return body except (HTTPError, URLError, TimeoutError) as exc: last=exc if isinstance(exc,HTTPError) and exc.code in (400,401,403,404): break time.sleep(min(2**i,12)) raise RuntimeError(f'Failed to fetch {url}: {last}') def parse_links(data,url): p=Links(); p.feed(data.decode('utf-8','replace')) return [(urljoin(url,href),label) for href,label in p.links] def allowed(url): return urlparse(url).hostname in ('www.oracle.com','oracle.com') and url.startswith('https://') def discover(index_html, archive_html, since): found={} for page,src in ((index_html,INDEX),(archive_html,ARCHIVE)): for url,label in parse_links(page,src): if not allowed(url): continue path=urlparse(url).path.lower() if not path.startswith('/security-alerts/') or not path.endswith('.html'): continue if path.endswith(('cpuarchive.html','index.html')): continue title=label.lower() if not any(x in title for x in ('critical patch update','critical security patch update','security alert','solaris third party bulletin')): continue # Years are from the advisory title, not CVE numbers or page modification date. years=[int(y) for y in YEAR.findall(label)] if not years or max(years)