#!/usr/bin/env python3
"""
Plot: mass launched to orbit vs disclosed satellite software vulnerabilities.

Data sources:
  - Mass to orbit: Jonathan McDowell's General Catalog of Artificial Space
    Objects (GCAT), https://planet4589.org/space/gcat/
    File: launch.tsv, column OrbPay (orbital payload mass in tonnes).
    Filtered to orbital launches (LaunchCode starts with 'O').

  - CVEs: NVD API (https://services.nvd.nist.gov/rest/json/cves/2.0)
    and GitHub Security Advisories (https://api.github.com).

    Reproducible queries (run 2025-02-25):
      NVD keywordSearch=CCSDS          -> 20 results
      NVD keywordSearch=CryptoLib      -> 28 results
      NVD keywordSearch=spacecraft     -> 22 results
      NVD keywordSearch=satellite      -> 119 results (high false-positive rate;
                                          includes IBM DB2 Satellite Admin etc.)
      NVD virtualMatchString=cpe:2.3:o:windriver:vxworks -> 39 results
      GitHub /repos/nasa/CryptoLib/security-advisories   -> 20 advisories

    The per-year counts below are curated from these queries plus conference
    disclosures not tracked in NVD. Each year is annotated with its sources.
    "satellite" keyword results were manually filtered to exclude non-space CVEs.

Usage:
  python3 satellite-cves.py            # fetches GCAT data, generates SVG+PNG
  python3 satellite-cves.py --cached   # uses cached GCAT data from /tmp
"""

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import urllib.request
import os
import sys
from collections import defaultdict
from pathlib import Path

# --- Mass to orbit (from GCAT) ---

GCAT_URL = 'https://planet4589.org/space/gcat/tsv/launch/launch.tsv'
GCAT_CACHE = '/tmp/gcat_launch.tsv'


def fetch_mass_by_year(use_cache=False):
    """Download GCAT launch.tsv and sum orbital payload mass by year."""
    cache = GCAT_CACHE
    if use_cache and os.path.exists(cache):
        print(f'Using cached {cache}')
    else:
        print(f'Downloading {GCAT_URL} ...')
        urllib.request.urlretrieve(GCAT_URL, cache)

    with open(cache) as f:
        lines = f.readlines()

    header = lines[0].lstrip('#').strip().split('\t')
    header = [h.strip() for h in header]
    orbpay_idx = header.index('OrbPay')
    lcode_idx = header.index('LaunchCode')
    date_idx = header.index('Launch_Date')

    mass_by_year = defaultdict(float)
    for line in lines[2:]:
        if not line.strip():
            continue
        fields = [f.strip() for f in line.split('\t')]
        if len(fields) <= max(orbpay_idx, lcode_idx, date_idx):
            continue
        if not fields[lcode_idx].startswith('O'):
            continue
        try:
            year = int(fields[date_idx][:4])
            mass = float(fields[orbpay_idx])
        except (ValueError, IndexError):
            continue
        mass_by_year[year] += mass

    return mass_by_year


# --- CVEs (curated from NVD + GitHub + conference disclosures) ---
#
# To reproduce, run:
#   curl 'https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=CCSDS&resultsPerPage=2000' | python3 -c "
#     import sys, json
#     for v in json.load(sys.stdin)['vulnerabilities']:
#       cve = v['cve']; print(f\"{cve['id']}\t{cve['published'][:10]}\t{cve['descriptions'][0]['value'][:100]}\")"
#
# Repeat for keywordSearch=CryptoLib, spacecraft, satellite, and:
#   curl 'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=cpe:2.3:o:windriver:vxworks&resultsPerPage=2000'
#
# For GitHub:
#   gh api '/repos/nasa/CryptoLib/security-advisories?per_page=100&state=published' --jq '.[] | {cve_id, published_at}'

CVE_DATA = {
    # 1960-2014: no publicly disclosed satellite software vulnerabilities.
    2015: 1,   # Turla APT satellite C2 hijacking (Kaspersky, Sep 2015)
    2017: 1,   # NotPetya (indirect satellite ground segment impact)
    2018: 2,   # IOActive SATCOM terminal vulns (Black Hat USA 2018)
    2019: 11,  # URGENT/11: 11 VxWorks CVEs (Armis, Jul 2019)
               # CVE-2019-12256 through CVE-2019-12263, plus others
    2020: 2,   # Hack-A-Sat 1; Pavur DVB-S eavesdropping (USENIX Sec 2020)
    2021: 1,   # Hack-A-Sat 2
    2022: 2,   # Viasat/KA-SAT AcidRain wiper (Feb 2022)
    2023: 15,  # Space Odyssey: 13 vulns in 3 satellites (IEEE S&P 2023);
               # Hack-A-Sat on-orbit (DEF CON 31)
    2024: 4,   # CryptoLib CVEs (GitHub/NVD); AIT-Core
    2025: 20,  # CryptoLib 12+ CVEs (GHSA, CVSS up to 9.8);
               # cFS Aquila RCE + path traversal + DoS (Black Hat 2025,
               # VisionSpace); total from NVD keywordSearch=CryptoLib
               # published in 2025 plus cFS conference disclosures
}

# --- Plot ---

OUT_DIR = Path(__file__).resolve().parent


def main():
    use_cache = '--cached' in sys.argv
    mass_by_year = fetch_mass_by_year(use_cache=use_cache)

    # Prepare mass data (2000 onwards)
    start_year, end_year = 2000, 2025
    years_mass = list(range(start_year, end_year + 1))
    mass_tonnes = [mass_by_year.get(y, 0) for y in years_mass]

    # Prepare CVE data
    years_cve = list(range(start_year, end_year + 1))
    cves = [CVE_DATA.get(y, 0) for y in years_cve]

    fig, ax1 = plt.subplots(figsize=(10, 5))
    fig.patch.set_facecolor('white')
    ax1.set_facecolor('#fafafa')
    ax1.grid(True, alpha=0.2, linewidth=0.5)

    # Mass (left axis)
    color_mass = '#4a7c9b'
    ax1.fill_between(years_mass, mass_tonnes, alpha=0.12, color=color_mass)
    ax1.plot(years_mass, mass_tonnes, color=color_mass, linewidth=2,
             label='Mass to orbit (tonnes/yr)')
    ax1.set_xlabel('Year', fontsize=10)
    ax1.set_ylabel('Mass to orbit (tonnes/yr)', color=color_mass, fontsize=10)
    ax1.tick_params(axis='y', labelcolor=color_mass)
    ax1.set_xlim(1999, 2027)
    max_mass = max(mass_tonnes) if mass_tonnes else 3200
    ax1.set_ylim(0, max_mass * 1.15)

    # CVEs (right axis)
    ax2 = ax1.twinx()
    color_cve = '#c0392b'
    bar_years = [y for y, c in zip(years_cve, cves) if c > 0]
    bar_counts = [c for c in cves if c > 0]
    ax2.bar(bar_years, bar_counts, color=color_cve, alpha=0.65, width=0.8,
            label='Disclosed vulnerabilities')
    ax2.set_ylabel('Disclosed vulnerabilities / yr', color=color_cve,
                    fontsize=10)
    ax2.tick_params(axis='y', labelcolor=color_cve)
    ax2.set_ylim(0, 38)

    # Annotations: vertical labels above each bar, no arrows
    label_style = dict(fontsize=7, ha='center', va='bottom',
                       rotation=90, color=color_cve)
    label_pad = 0.8  # gap above bar top
    for year, label in [(2019, 'URGENT/11 (VxWorks)'),
                        (2022, 'Viasat'),
                        (2023, 'Space Odyssey'),
                        (2025, 'CryptoLib + cFS')]:
        count = CVE_DATA.get(year, 0)
        ax2.text(year, count + label_pad, label, **label_style)

    # Title
    fig.suptitle(
        'Mass Launched to Orbit vs. Disclosed Satellite Software Vulnerabilities',
        fontsize=11, fontweight='bold', y=0.98)

    # Legend
    lines1, labels1 = ax1.get_legend_handles_labels()
    lines2, labels2 = ax2.get_legend_handles_labels()
    ax1.legend(lines1 + lines2, labels1 + labels2,
               loc='upper left', fontsize=8, framealpha=0.9,
               bbox_to_anchor=(0.0, 0.88))

    plt.tight_layout(rect=[0, 0, 1, 0.96])

    # Save
    for fmt in ['svg', 'png']:
        out = OUT_DIR / f'satellite-cves.{fmt}'
        fig.savefig(str(out), format=fmt, bbox_inches='tight', dpi=150)
        print(f'Saved {out}')


if __name__ == '__main__':
    main()
