""" Autarch CVE Database — CVE lookup for Android packages via OSV.dev. by: SsSnake """ import json import os import threading import urllib.request import urllib.error from datetime import datetime, timezone from pathlib import Path _db_instance = None _db_lock = threading.Lock() def get_cve_db(): global _db_instance if _db_instance is None: with _db_lock: if _db_instance is None: _db_instance = CveDatabase() return _db_instance class CveDatabase: """CVE lookup for Android packages. Uses OSV.dev as primary data source.""" DATA_DIR = Path(__file__).parent.parent / 'data' / 'ioc' / 'cve' OSV_QUERY_URL = 'https://api.osv.dev/v1/query' OSV_BATCH_URL = 'https://api.osv.dev/v1/querybatch' def __init__(self): self.DATA_DIR.mkdir(parents=True, exist_ok=True) self._cache = {} # package_name -> [vuln_dicts] self._stats_cache = None self._lock = threading.Lock() self._last_query = None def get_cve_for_package(self, package_name, version=None) -> list: """Query OSV.dev for CVEs affecting an Android package.""" with self._lock: if package_name in self._cache: return self._cache[package_name] try: body = { "package": { "name": package_name, "ecosystem": "Android" } } if version is not None: body["version"] = version response = self._query_osv(body) vulns = response.get("vulns", []) results = [] for vuln in vulns: parsed = self._parse_vuln(vuln, package_name) results.append(parsed) with self._lock: self._cache[package_name] = results self._last_query = datetime.now(timezone.utc).isoformat() return results except Exception: return [] def get_cve_for_android_sdk(self, sdk_version) -> list: """Query OSV.dev for CVEs affecting a specific Android SDK version.""" package_name = f"android:{sdk_version}" try: body = { "package": { "name": package_name, "ecosystem": "Android" } } response = self._query_osv(body) vulns = response.get("vulns", []) results = [] for vuln in vulns: parsed = self._parse_vuln(vuln, package_name) results.append(parsed) with self._lock: self._cache[package_name] = results self._last_query = datetime.now(timezone.utc).isoformat() return results except Exception: return [] def check_packages_batch(self, packages: list) -> dict: """Query OSV.dev for CVEs affecting multiple Android packages at once.""" if not packages: return {} try: results = {} # Split into chunks of 50 chunk_size = 50 for i in range(0, len(packages), chunk_size): chunk = packages[i:i + chunk_size] queries = [ {"package": {"name": p["name"], "ecosystem": "Android"}} for p in chunk ] body = {"queries": queries} data = json.dumps(body).encode("utf-8") req = urllib.request.Request( self.OSV_BATCH_URL, data=data, headers={ "Content-Type": "application/json", "User-Agent": "Autarch/1.0" }, method="POST" ) try: with urllib.request.urlopen(req, timeout=10) as resp: response = json.loads(resp.read().decode("utf-8")) except Exception: continue batch_results = response.get("results", []) for idx, result in enumerate(batch_results): if idx >= len(chunk): break pkg_name = chunk[idx]["name"] vulns = result.get("vulns", []) parsed_vulns = [self._parse_vuln(v, pkg_name) for v in vulns] results[pkg_name] = parsed_vulns with self._lock: self._cache[pkg_name] = parsed_vulns with self._lock: if results: self._last_query = datetime.now(timezone.utc).isoformat() return results except Exception: return {} def get_stats(self) -> dict: """Return basic stats about the CVE database cache.""" with self._lock: return { "total_cached": len(self._cache), "last_query": self._last_query, "data_dir": str(self.DATA_DIR) } def _query_osv(self, body: dict) -> dict: """POST a JSON body to the OSV.dev query endpoint. Returns parsed JSON or {}.""" try: data = json.dumps(body).encode("utf-8") req = urllib.request.Request( self.OSV_QUERY_URL, data=data, headers={ "Content-Type": "application/json", "User-Agent": "Autarch/1.0" }, method="POST" ) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode("utf-8")) except Exception: return {} def _parse_vuln(self, vuln: dict, package_name: str) -> dict: """Extract standardised CVE fields from an OSV vulnerability object.""" # cve_id: prefer alias starting with "CVE-", else use vuln id cve_id = vuln.get("id", "UNKNOWN") for alias in vuln.get("aliases", []): if alias.startswith("CVE-"): cve_id = alias break # severity: try database_specific first, then severity array score, normalise raw_severity = ( vuln.get("database_specific", {}).get("severity") or vuln.get("severity", [{}])[0].get("score", "UNKNOWN") ) severity = self._normalise_severity(raw_severity) # description: summary or details, capped at 500 chars description = vuln.get("summary", vuln.get("details", ""))[:500] # patched_version: look for a "fixed" event in affected ranges patched_version = "unknown" affected_list = vuln.get("affected", [{}]) if affected_list: ranges = affected_list[0].get("ranges", []) for r in ranges: for event in r.get("events", []): if "fixed" in event: patched_version = event["fixed"] break if patched_version != "unknown": break return { "package": package_name, "cve_id": cve_id, "severity": severity, "description": description, "patched_version": patched_version, "source": "osv.dev" } def _normalise_severity(self, raw) -> str: """Map a raw severity string or CVSS score to critical/high/medium/low.""" if raw is None or raw == "UNKNOWN": return "medium" raw_str = str(raw).strip().upper() # Direct label match label_map = { "CRITICAL": "critical", "HIGH": "high", "MEDIUM": "medium", "MODERATE": "medium", "LOW": "low", "NONE": "low" } if raw_str in label_map: return label_map[raw_str] # Try numeric CVSS score try: score = float(raw_str) if score >= 9.0: return "critical" elif score >= 7.0: return "high" elif score >= 4.0: return "medium" else: return "low" except ValueError: pass # CVSS vector strings often start with digits after the last colon # e.g. "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" # Fall back to medium if we can't parse it return "medium"