{"uuid": "f4fbd8db-fe3c-4fa5-a7f0-20014c77d534", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2004-1653", "type": "seen", "source": "https://gist.github.com/shino/5cefdef6d36283f50795e32ffcf1a51d", "content": "#!/usr/bin/env python3\n\"\"\"Generate the fortinet-cvrf supplement table.\n\nOld Fortinet CVRF advisories (2012-2021 and a few 2022) carry no\nproduct_statuses/product_tree, so `vuls-data-update extract fortinet-cvrf`\nemits them content-only. This script derives the missing affected-product data\nfor those advisories from two sources and emits a static supplement table to be\nembedded in vuls-data-update:\n\n  1. the legacy curated handmade dataset (vuls-data-raw-fortinet) \u2014 good\n     version ranges for ~2012-2018, but 2019+ entries are mostly empty and\n     sometimes list not-impacted products, so only nodes with an actual\n     version constraint are used;\n  2. Fortinet's own CNA records in cvelistV5 (containers.cna.affected) \u2014\n     covers ~2016-2022; only CVEs assigned by the \"fortinet\" CNA are used\n     (other assigners describe the upstream product, not Fortinet's).\n\nPer advisory, handmade wins when it has constrained nodes; CNA fills the rest.\nAdvisories where both exist are cross-checked and diffs land in report.md for\nhuman review.\n\nUsage:\n  ./fetch_inputs.sh\n  ./generate.py --fetch-cves   # downloads cvelistV5 records for the gap CVEs\n  ./generate.py                # writes supplement_data.go (for the cvrf package),\n                               # supplement.json (tooling byproduct), report.md\n\nThe output supplement.json maps advisory ID -&gt; product rows:\n  {\"FG-IR-012-001\": [{\"product\": \"FortiOS\", \"ranges\": [{\"ge\": \"4.3.0\", \"le\": \"4.3.5\"}]}]}\nwhere \"product\" is a key of vuls-data-update's fortinet/internal/product table\n(this script parses table.go and hard-fails on any name/CPE it cannot map).\n\"\"\"\n\nimport argparse\nimport collections\nimport glob\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nWORK = os.path.join(HERE, \"work\")\nHM = os.path.join(WORK, \"vuls-data-raw-fortinet\")\nCVRF = os.path.join(WORK, \"vuls-data-raw-fortinet-cvrf\")\nCVELIST = os.path.join(WORK, \"cvelist\")\nTABLE_GO = os.path.normpath(os.path.join(\n    HERE, \"../../vuls-data-update/pkg/extract/fortinet/internal/product/table.go\"))\n\nCVE_RE = re.compile(r\"CVE-(?:)?(\\d{4})-(\\d+)\")\nVER_RE = re.compile(r\"^\\d+(\\.\\d+){0,3}$\")\n\n\n# --- product table ----------------------------------------------------------\n\ndef load_table():\n    \"\"\"Parse table.go: name -&gt; (cpe, twoComponentVersions).\"\"\"\n    src = open(TABLE_GO).read()\n    entries = {}\n    for m in re.finditer(\n            r'\"([^\"]+)\":\\s*\\{cpe: \"([^\"]+)\"(.*?)\\},\\n', src):\n        entries[m.group(1)] = (m.group(2), \"twoComponentVersions: true\" in m.group(3))\n    if not entries:\n        sys.exit(f\"could not parse any entries from {TABLE_GO}\")\n    return entries\n\n\ndef reverse_table(entries):\n    \"\"\"(vendor, product) -&gt; canonical product name. Handmade CPEs vary in part\n    (a: vs o:) and carry target_sw/edition qualifiers the curated table CPEs do\n    not, so match on vendor:product only. For vendor:product pairs shared by\n    several names any name resolves to the same table CPE; the plain\n    OS-agnostic-looking one is preferred via the explicit map.\"\"\"\n    prefer = {\n        (\"fortinet\", \"forticlient\"): \"FortiClientWindows\",\n    }\n    rev = {}\n    for name, (cpe, _) in sorted(entries.items()):\n        parts = cpe.split(\":\")\n        rev.setdefault((parts[3], parts[4]), name)\n    rev.update(prefer)\n    return rev\n\n\n# Handmade FortiClient nodes carry the platform in target_sw; keep it.\nFORTICLIENT_TARGET_SW = {\n    \"windows\": \"FortiClientWindows\",\n    \"macos\": \"FortiClientMac\",\n    \"mac_os_x\": \"FortiClientMac\",\n    \"linux\": \"FortiClientLinux\",\n    \"android\": \"FortiClientAndroid\",\n    \"ios\": \"FortiClientiOS\",\n}\n\n\ndef handmade_product_name(cpe, rev):\n    parts = cpe.split(\":\")\n    vendor, product, target_sw = parts[3], parts[4], parts[10]\n    if (vendor, product) == (\"fortinet\", \"forticlient\") and target_sw in FORTICLIENT_TARGET_SW:\n        return FORTICLIENT_TARGET_SW[target_sw]\n    return rev.get((vendor, product))\n\n\n# --- corpus helpers ---------------------------------------------------------\n\ndef load_json_tree(root):\n    out = {}\n    for p in glob.glob(root + \"/[0-9]*/**/*.json\", recursive=True):\n        out[os.path.splitext(os.path.basename(p))[0]] = p\n    return out\n\n\ndef cvrf_statuses_empty(doc):\n    st = (doc.get(\"vulnerability\") or {}).get(\"product_statuses\")\n    if isinstance(st, list):\n        return not any((s.get(\"status\") or {}) for s in st)\n    if isinstance(st, dict):\n        s = st.get(\"status\") or {}\n        if isinstance(s, list):\n            return not any(x for x in s)\n        return not s\n    return True\n\n\ndef cvrf_cves(doc):\n    cves = (doc.get(\"vulnerability\") or {}).get(\"cve\") or []\n    if isinstance(cves, str):\n        cves = [cves]\n    out = []\n    for c in cves:\n        m = CVE_RE.search(c)\n        if m:\n            out.append(f\"CVE-{m.group(1)}-{m.group(2)}\")\n    return sorted(set(out))\n\n\n# --- handmade rows ----------------------------------------------------------\n\ndef handmade_nodes(path):\n    d = json.load(open(path))\n    for v in d.get(\"vulnerabilities\") or []:\n        for de in v.get(\"definitions\") or []:\n            for c in de.get(\"configurations\") or []:\n                yield from (c.get(\"nodes\") or [])\n\n\ndef train_range(token):\n    \"\"\"'6.0' -&gt; {ge: 6.0, lt: 6.1}; '5' -&gt; {ge: 5, lt: 6}.\"\"\"\n    parts = token.split(\".\")\n    nxt = parts[:-1] + [str(int(parts[-1]) + 1)]\n    return {\"ge\": token, \"lt\": \".\".join(nxt)}\n\n\nDASH_BUILD_RE = re.compile(r\"^(\\d+\\.\\d+)-(\\d+)-(\\d+)$\")\n\n\ndef normalize_version(v):\n    \"\"\"Old Meru-style dash builds (\"8.2-4-0\", FortiWLC/AscenLink era) are the\n    dotted builds of the same product in later notation -&gt; \"8.2.4.0\".\"\"\"\n    m = DASH_BUILD_RE.match(v)\n    if m:\n        return f\"{m.group(1)}.{m.group(2)}.{m.group(3)}\"\n    return v\n\n\ndef handmade_rows(aid, hm_paths, rev, table, problems, notes):\n    \"\"\"Rows from the handmade dataset; only nodes with a version constraint.\"\"\"\n    if aid not in hm_paths:\n        return []\n    per_product = collections.defaultdict(lambda: {\"exacts\": [], \"ranges\": []})\n    for n in handmade_nodes(hm_paths[aid]):\n        a = n.get(\"affected\") or {}\n        a = {k: normalize_version((a.get(k) or \"\").strip()) for k in (\"eq\", \"gt\", \"ge\", \"lt\", \"le\")}\n        if not any(a.values()):\n            continue\n        cpe = (n.get(\"cpe\") or \"\").strip()\n        if cpe.startswith(\"cpe:2.3:h:\"):\n            # Hardware appliance models (version \"-\"); scanners report the\n            # software CPE, so hardware rows are not usable for detection.\n            notes.append(f\"{aid}: skipped hardware cpe {cpe}\")\n            continue\n        name = handmade_product_name(cpe, rev)\n        if name is None:\n            problems.append(f\"{aid}: handmade cpe not in product table: {cpe}\")\n            continue\n        if a[\"eq\"] == \"ANY\":\n            # Handmade notation for \"every version of this product\".\n            per_product[name][\"all\"] = True\n            continue\n        if \":beta\" in a[\"eq\"]:\n            # Handmade enumerates FortiOS 5.0 betas for FG-IR-012-003; beta\n            # builds are not scanner-reportable versions.\n            notes.append(f\"{aid}: skipped beta version {a['eq']!r} for {name}\")\n            continue\n        for k, v in a.items():\n            if v and not VER_RE.match(v):\n                problems.append(f\"{aid}: handmade non-numeric bound {k}={v!r} for {name}\")\n                a[k] = \"\"\n        entry = per_product[name]\n        if a[\"eq\"]:\n            two = table[name][1]\n            dots = a[\"eq\"].count(\".\")\n            if dots &gt;= (1 if two else 2):\n                entry[\"exacts\"].append(a[\"eq\"])\n            else:\n                # A bare train as an exact enumeration covers the whole train.\n                entry[\"ranges\"].append(train_range(a[\"eq\"]))\n        r = {k: v for k, v in a.items() if v and k != \"eq\"}\n        if r:\n            entry[\"ranges\"].append(r)\n    return finish_rows(per_product)\n\n\n# --- CNA rows ---------------------------------------------------------------\n\n# CNA affected[].product / free-text product tokens -&gt; product table name(s).\n# A single CNA token can stand for several products (\"FortiAP-S/W2\").\nCNA_PRODUCT_ALIASES = {\n    \"FortiGate\": [\"FortiOS\"],\n    \"FortiAnalyzer BigData\": [\"FortiAnalyzer-BigData\"],\n    \"FortiClient\": [\"FortiClientWindows\"],\n    \"FortiClient for Windows\": [\"FortiClientWindows\"],\n    \"FortiClient Windows\": [\"FortiClientWindows\"],\n    \"FortiClient for Mac\": [\"FortiClientMac\"],\n    \"FortiClient for Mac OS\": [\"FortiClientMac\"],\n    \"FortiClient for Mac OSX\": [\"FortiClientMac\"],\n    \"FortiClient for MacOSX\": [\"FortiClientMac\"],\n    \"FortiClient MacOS\": [\"FortiClientMac\"],\n    \"FortiClient for Linux\": [\"FortiClientLinux\"],\n    \"FortiClient Linux\": [\"FortiClientLinux\"],\n    \"FortiClient iOS\": [\"FortiClientiOS\"],\n    \"FortiClient for iOS\": [\"FortiClientiOS\"],\n    \"FortiClient Android\": [\"FortiClientAndroid\"],\n    \"FortiClient for Android\": [\"FortiClientAndroid\"],\n    \"FortiClient EMS\": [\"FortiClientEMS\"],\n    \"FortiClient SSLVPN Client for Linux\": [\"FortiClientSSLVPN\"],\n    \"FortiVoiceEnterprise\": [\"FortiVoice\"],\n    \"FortiAuthenticator WEB UI\": [\"FortiAuthenticator\"],\n    \"WindowsAgent\": [\"FortiSIEMWindowsAgent\"],\n    \"FSSO Windows DC Agent\": [\"FSSO\"],\n    \"FSSO Windows CA\": [\"FSSO CA\"],\n    \"FortiAP-S/W2\": [\"FortiAP-S\", \"FortiAP-W2\"],\n}\n\n\ndef cna_product_names(raw, table, problems, ctx):\n    \"\"\"Resolve a CNA product token to table names, or None on failure.\n    The token may list several products (\"FortiOS and FortiProxy\").\"\"\"\n    s = re.sub(r\"\\s+\", \" \", raw).strip().strip(\",\")\n    while s.lower().startswith(\"fortinet \"):\n        s = s[len(\"fortinet \"):]\n    if not s or s.lower() == \"n/a\":\n        return []\n    if s in CNA_PRODUCT_ALIASES:\n        return CNA_PRODUCT_ALIASES[s]\n    if s in table:\n        return [s]\n    # A product field that itself enumerates products.\n    parts = [p for p in re.split(r\",|;|\\band\\b\", s) if p.strip()]\n    if len(parts) &gt; 1:\n        out = []\n        for p in parts:\n            names = cna_product_names(p, table, problems, ctx)\n            if names is None:\n                return None\n            out.extend(names)\n        return out\n    problems.append(f\"{ctx}: unmapped CNA product name {raw!r} (normalized {s!r})\")\n    return None\n\n\ndef parse_cna_version_text(text, default_products, table, problems, ctx):\n    \"\"\"Parse one CNA free-text version value into {product: {exacts, ranges}}.\n\n    Grammar (phrase-level, hard-listed): a stream of product-name tokens and\n    version phrases. A product token switches the current product(s); version\n    phrases attach to them. Anything outside the grammar is reported and the\n    whole value is rejected (rows come from a manual override instead).\n    \"\"\"\n    out = collections.defaultdict(lambda: {\"exacts\": [], \"ranges\": []})\n    s = re.sub(r\"\\s+\", \" \", text).strip().rstrip(\".\")\n    s = re.sub(r\"\\b(\\d+\\.\\d+)-(\\d+)-(\\d+)\\b\", r\"\\1.\\2.\\3\", s)  # Meru dash builds\n    if not s:\n        return out\n    if s.lower().startswith(\"fixed in\"):\n        # A fix note miscoded as an affected version entry; carries no\n        # affected-version information.\n        return out\n\n    # Tokenize into product switches and version phrases. Longest-first so\n    # \"FortiAnalyzer-BigData\" wins over \"FortiAnalyzer\".\n    prod_tokens = sorted(\n        set(list(CNA_PRODUCT_ALIASES) + list(table)), key=len, reverse=True)\n    prod_re = \"|\".join(re.escape(p) for p in prod_tokens)\n    ver = r\"\\d+(?:\\.\\d+){0,3}\"\n    train = r\"\\d+(?:\\.\\d+)?\"\n    below = r\"(?:below|earlier|lower|prior)\"\n    through = r\"(?:through|thorugh|to)\"\n    phrase_res = [\n        # \"1.2.3 through 4.5.x\"\n        (re.compile(rf\"({ver})\\s+{through}\\s+({train})\\.x\\b\", re.I),\n         lambda m: [(\"range\", {\"ge\": m.group(1), \"lt\": train_range(m.group(2))[\"lt\"]})]),\n        # \"1.2.3 through 4.5.6\" / \"1.2.3 to 4.5.6\"\n        (re.compile(rf\"({ver})\\s+{through}\\s+({ver})\", re.I),\n         lambda m: [(\"range\", {\"ge\": m.group(1), \"le\": m.group(2)})]),\n        # \"1.2.3 and below/earlier/lower/prior\" (also \"6.0.11and below\")\n        (re.compile(rf\"({ver})\\s*and\\s+{below}\", re.I),\n         lambda m: [(\"range\", {\"le\": m.group(1)})]),\n        # \"1.2.3 and above/later\"\n        (re.compile(rf\"({ver})\\s*and\\s+(?:above|later)\", re.I),\n         lambda m: [(\"range\", {\"ge\": m.group(1)})]),\n        # \"all versions below/before/prior to 1.2.3\"\n        (re.compile(rf\"all versions\\s+(?:below|before|prior to|lower than)\\s+(?:version\\s+)?({ver})\", re.I),\n         lambda m: [(\"range\", {\"lt\": m.group(1)})]),\n        # \"below/before/prior to 1.2.3\"\n        (re.compile(rf\"(?:below|before|prior to|lower than)\\s+(?:version\\s+)?({ver})\", re.I),\n         lambda m: [(\"range\", {\"lt\": m.group(1)})]),\n        # \"all versions of\" \u2014 a train list follows; consume the phrase only.\n        (re.compile(r\"all versions of\\b\", re.I), lambda m: []),\n        # \"all versions\"\n        (re.compile(r\"all versions\\b\", re.I), lambda m: [(\"all\", None)]),\n        # \"5.x\" / \"6.0.x\" train wildcard\n        (re.compile(rf\"({train})\\.x\\b\", re.I),\n         lambda m: [(\"range\", train_range(m.group(1)))]),\n        # bare version\n        (re.compile(rf\"({ver})\"), lambda m: [(\"ver\", m.group(1))]),\n    ]\n\n    pos, cur = 0, list(default_products)\n    while pos &lt; len(s):\n        rest = s[pos:]\n        m = re.match(r\"(?:\\s+|,|;|&amp;|\\band\\b|\\bversions?\\b)+\", rest, re.I)\n        if m:\n            pos += m.end()\n            continue\n        m = re.match(prod_re, rest)\n        if m:\n            names = cna_product_names(m.group(0), table, problems, ctx)\n            if names is None:\n                return None\n            cur = names\n            pos += m.end()\n            continue\n        matched = False\n        for rx, mk in phrase_res:\n            m = rx.match(rest)\n            if m:\n                for kind, val in mk(m):\n                    if not cur:\n                        problems.append(f\"{ctx}: version phrase with no product: {rest[:60]!r}\")\n                        return None\n                    for name in cur:\n                        if kind == \"range\":\n                            out[name][\"ranges\"].append(dict(val))\n                        elif kind == \"all\":\n                            out[name][\"all\"] = True\n                        else:\n                            two = table[name][1]\n                            if val.count(\".\") &gt;= (1 if two else 2):\n                                out[name][\"exacts\"].append(val)\n                            else:\n                                out[name][\"ranges\"].append(train_range(val))\n                pos += m.end()\n                matched = True\n                break\n        if not matched:\n            problems.append(f\"{ctx}: unparsed version text at {rest[:80]!r} (full: {s[:160]!r})\")\n            return None\n    return out\n\n\ndef structured_range(lo, lte, lt, problems, ctx):\n    \"\"\"CNA structured bounds -&gt; range dict, or None on a bound outside the\n    grammar. A trailing \".*\"/\".x\" on an inclusive upper bound means \"any\n    release of that train\" -&gt; lt the next train.\"\"\"\n    r = {}\n    if lo and lo not in (\"unspecified\", \"0\", \"*\", \"n/a\"):\n        if not VER_RE.match(lo):\n            problems.append(f\"{ctx}: bad structured lower bound {lo!r}\")\n            return None\n        r[\"ge\"] = lo\n    b = lte or lt\n    m = re.fullmatch(r\"(\\d+(?:\\.\\d+)?)[.](?:\\*|x)\", b)\n    if m:\n        # CVE JSON convention: a wildcard upper bound spans the whole train\n        # ({version: 6.2.0, lessThan: \"6.2.*\"} = the entire 6.2 train), on\n        # either the inclusive or the exclusive field.\n        r[\"lt\"] = train_range(m.group(1))[\"lt\"]\n        return r\n    if not VER_RE.match(b):\n        problems.append(f\"{ctx}: bad structured upper bound {b!r}\")\n        return None\n    r[\"le\" if lte else \"lt\"] = b\n    return r\n\n\ndef cna_rows(aid, cves, table, problems):\n    \"\"\"Rows from the Fortinet CNA records, or None when any part of the\n    advisory's CNA data falls outside the grammar (partial rows would look\n    complete; a manual override supplies such advisories instead).\"\"\"\n    per_product = collections.defaultdict(lambda: {\"exacts\": [], \"ranges\": []})\n    used, failed = False, False\n    for cve in cves:\n        p = os.path.join(CVELIST, cve + \".json\")\n        if not os.path.exists(p):\n            continue\n        try:\n            doc = json.load(open(p))\n        except json.JSONDecodeError:\n            continue\n        if doc.get(\"cveMetadata\", {}).get(\"assignerShortName\") != \"fortinet\":\n            continue\n        for a in doc.get(\"containers\", {}).get(\"cna\", {}).get(\"affected\", []) or []:\n            ctx = f\"{aid}/{cve}\"\n            defaults = cna_product_names(a.get(\"product\", \"\"), table, problems, ctx)\n            if defaults is None:\n                failed = True\n                defaults = []\n            for v in a.get(\"versions\", []) or []:\n                if v.get(\"status\") not in (None, \"affected\"):\n                    continue\n                lo = (v.get(\"version\") or \"\").strip()\n                lte = (v.get(\"lessThanOrEqual\") or \"\").strip()\n                lt = (v.get(\"lessThan\") or \"\").strip()\n                if lte or lt:\n                    if not defaults:\n                        problems.append(f\"{ctx}: structured range with no product\")\n                        failed = True\n                        continue\n                    r = structured_range(lo, lte, lt, problems, ctx)\n                    if r is None:\n                        failed = True\n                        continue\n                    for name in defaults:\n                        per_product[name][\"ranges\"].append(dict(r))\n                    used = True\n                    continue\n                if not lo or lo.lower() in (\"n/a\", \"unspecified\"):\n                    continue\n                parsed = parse_cna_version_text(lo, defaults, table, problems, ctx)\n                if parsed is None:\n                    failed = True\n                    continue\n                for name, e in parsed.items():\n                    per_product[name][\"exacts\"].extend(e[\"exacts\"])\n                    per_product[name][\"ranges\"].extend(e[\"ranges\"])\n                    if e.get(\"all\"):\n                        per_product[name][\"all\"] = True\n                    used = True\n    if failed:\n        return None\n    if not used:\n        return []\n    return finish_rows(per_product)\n\n\n# --- manual overrides -------------------------------------------------------\n\n# Advisory rows curated by hand for the tail neither source yields cleanly.\n# Rows are hand-derived from the advisory's CVRF \"Affected Products\" note\n# unless stated otherwise; keep a short provenance comment on every entry.\nMANUAL_OVERRIDES = {\n    # Handmade enumerates AscenLink dash builds within the 7.1 train\n    # (eq 7.1-B5599); span the train instead of guessing build ordering.\n    \"FG-IR-14-011\": [{\"product\": \"AscenLink\", \"ranges\": [{\"ge\": \"7.1\", \"lt\": \"7.2\"}]}],\n    # Handmade: lt 7.1-b5955 (fixed within the 7.1 train); same rationale.\n    \"FG-IR-14-018\": [{\"product\": \"AscenLink\", \"ranges\": [{\"ge\": \"7.1\", \"lt\": \"7.2\"}]}],\n    # \"Standalone Forticlient SSLVPN Linux client build 2312 and lower\", fixed\n    # in build 2313. The advisory (and handmade) names only the bare build\n    # number, but the product versions as \"major.minor.build\" with one\n    # monotonic build sequence, and the 4.0 prefix is attested on both sides\n    # of this 2015 advisory: 4.0.2012/4.0.2258 (FG-IR-13-008, 2013) and\n    # 4.0.2328/4.0.2332 (FG-IR-16-048, 2016; 4.4 only starts at 4.4.2334 in\n    # FG-IR-17-214). So build 2312 is 4.0.2312, and the full-form bound is\n    # used \u2014 handmade's bare \"le 2312\" would numerically swallow every\n    # full-form version (4 &lt; 2312), including the not-affected 4.0.2328.\n    \"FG-IR-15-017\": [{\"product\": \"FortiClientSSLVPN\", \"ranges\": [{\"le\": \"4.0.2312\"}]}],\n    # \"FortiCASB all versions below 4.1.0\"\n    \"FG-IR-19-001\": [{\"product\": \"FortiCASB\", \"ranges\": [{\"lt\": \"4.1.0\"}]}],\n    # \"FortiClient for Windows (6.2.0 and earlier) / for Mac OSX (6.2.0 and earlier)\"\n    \"FG-IR-19-110\": [{\"product\": \"FortiClientMac\", \"ranges\": [{\"le\": \"6.2.0\"}]},\n                     {\"product\": \"FortiClientWindows\", \"ranges\": [{\"le\": \"6.2.0\"}]}],\n    # \"FortiManager 6.2.1 and below\"\n    \"FG-IR-19-271\": [{\"product\": \"FortiManager\", \"ranges\": [{\"le\": \"6.2.1\"}]}],\n    # \"FortiAnalyzer 6.2.0 to 6.2.3, 6.0.8 and below\" + same for FortiManager\n    \"FG-IR-19-294\": [{\"product\": \"FortiAnalyzer\", \"ranges\": [{\"ge\": \"6.2.0\", \"le\": \"6.2.3\"}, {\"le\": \"6.0.8\"}]},\n                     {\"product\": \"FortiManager\", \"ranges\": [{\"ge\": \"6.2.0\", \"le\": \"6.2.3\"}, {\"le\": \"6.0.8\"}]}],\n    # \"FortiSIEM version 5.2.8 and below\"\n    \"FG-IR-20-041\": [{\"product\": \"FortiSIEM\", \"ranges\": [{\"le\": \"5.2.8\"}]}],\n    # \"FortiGate Cloud Version 20.3 and below\"\n    \"FG-IR-20-193\": [{\"product\": \"FortiGate Cloud\", \"ranges\": [{\"le\": \"20.3\"}]}],\n    # \"FortiAuthenticator 6.3.2 and below\" (the 6.2.x/6.1.x/6.0.x lines are subsumed)\n    \"FG-IR-20-217\": [{\"product\": \"FortiAuthenticator\", \"ranges\": [{\"le\": \"6.3.2\"}]}],\n    # \"FortiWLC versions 8.6.0 and below\" (8.5.3 line subsumed)\n    \"FG-IR-21-001\": [{\"product\": \"FortiWLC\", \"ranges\": [{\"le\": \"8.6.0\"}]}],\n    # \"FortiExtender version 7.0.1 and below\" (4.2.3/4.1.7 lines subsumed)\n    \"FG-IR-21-148\": [{\"product\": \"FortiExtender\", \"ranges\": [{\"le\": \"7.0.1\"}]}],\n    # \"FortiWeb 6.4.1 and below\" (older lines subsumed)\n    \"FG-IR-21-158\": [{\"product\": \"FortiWeb\", \"ranges\": [{\"le\": \"6.4.1\"}]}],\n    # \"FortiWeb version 6.4.1 and below / 6.3.15 and below\" (subsumed)\n    \"FG-IR-21-166\": [{\"product\": \"FortiWeb\", \"ranges\": [{\"le\": \"6.4.1\"}]}],\n    # \"FortiWeb version 5.9.0 through 5.9.1 / 6.0.0-6.0.7 / 6.1.0-6.1.2 /\n    #  6.2.0-6.2.6 / 6.3.0-6.3.16 / 6.4.0-6.4.1\"\n    \"FG-IR-21-180\": [{\"product\": \"FortiWeb\", \"ranges\": [\n        {\"ge\": \"5.9.0\", \"le\": \"5.9.1\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.7\"},\n        {\"ge\": \"6.1.0\", \"le\": \"6.1.2\"}, {\"ge\": \"6.2.0\", \"le\": \"6.2.6\"},\n        {\"ge\": \"6.3.0\", \"le\": \"6.3.16\"}, {\"ge\": \"6.4.0\", \"le\": \"6.4.1\"}]}],\n}\n\n# Advisories whose handmade rows contradict the advisory text; use CNA only.\nHANDMADE_EXCLUDED = {\n    # Handmade lists FortiOS, but the advisory's Affected Products note (and\n    # the CNA record) lists only FortiManager/FortiAnalyzer.\n    \"FG-IR-21-206\",\n    # Handmade inverted the FortiDeceptor 3.0/3.1 bounds (ge 3.0.2 le 3.0.0,\n    # ge 3.1.1 le 3.1.0) and marked 3.3.3 fixed where the note says affected\n    # (\"3.3.0 through 3.3.3\"); the CNA record matches the Affected Products\n    # note exactly and carries more FortiSandbox ranges than handmade.\n    \"FG-IR-22-056\",\n    # Handmade lists FortiOS 5.0.3-5.0.5, unrelated to this advisory; the CVE\n    # record (CVE-2019-15703) reads \"FortiOS 6.2.1, 6.2.0, 6.0.8 and below\",\n    # which is what the CNA rows carry.\n    \"FG-IR-19-186\",\n}\n\n# Per-(advisory, product) row replacements for rows a source got wrong while\n# the rest of the advisory's rows are fine. Applied after the union merge.\nPRODUCT_ROW_FIXUPS = {\n    # The Affected Products note reads \"FortiWAN version 4.5.0 through 4.5.9 /\n    # 4.4 all versions / 4.3 all versions\"; handmade corrupted the 4.3 row\n    # into the inverted \"ge 4.4.0, le 4.3.1\" and narrowed 4.4 to eq 4.4.0.\n    # Model 4.3/4.4 as trains, and keep handmade's extra 4.0-4.2 ranges (they\n    # match its per-train style elsewhere and only widen coverage for EOL\n    # trains of this discontinued product).\n    (\"FG-IR-22-059\", \"FortiWAN\"): [{\n        \"product\": \"FortiWAN\",\n        \"ranges\": [{\"ge\": \"4.0.0\", \"le\": \"4.0.6\"}, {\"ge\": \"4.1.0\", \"le\": \"4.1.3\"},\n                   {\"ge\": \"4.2.0\", \"le\": \"4.2.7\"}, {\"ge\": \"4.3.0\", \"lt\": \"4.4\"},\n                   {\"ge\": \"4.4.0\", \"lt\": \"4.5\"}, {\"ge\": \"4.5.0\", \"le\": \"4.5.9\"}],\n    }],\n    # The note reads \"5.2 and below\" (like FG-IR-17-172, where handmade used a\n    # bare le); handmade's ge 5.2.0 cut off the older trains the note includes.\n    (\"FG-IR-17-245\", \"FortiOS\"): [{\n        \"product\": \"FortiOS\",\n        \"ranges\": [{\"le\": \"5.2.15\"}, {\"ge\": \"5.4.0\", \"le\": \"5.4.8\"},\n                   {\"ge\": \"5.6.0\", \"le\": \"5.6.2\"}],\n    }],\n    # The note names no versions (\"All FortiClient for Windows which has\n    # Vulnerability scan features enabled\"); the CNA record extends handmade's\n    # le 6.0.4 to 6.0.5.\n    (\"FG-IR-18-108\", \"FortiClientWindows\"): [{\n        \"product\": \"FortiClientWindows\", \"ranges\": [{\"le\": \"6.0.5\"}],\n    }],\n    # The note hedges with \"At least ... 6.0.0 through 6.0.4\"; the CNA record\n    # additionally lists 5.6.0 through 5.6.11 for both products.\n    (\"FG-IR-18-232\", \"FortiAnalyzer\"): [{\n        \"product\": \"FortiAnalyzer\",\n        \"ranges\": [{\"ge\": \"5.6.0\", \"le\": \"5.6.11\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.4\"}],\n    }],\n    (\"FG-IR-18-232\", \"FortiManager\"): [{\n        \"product\": \"FortiManager\",\n        \"ranges\": [{\"ge\": \"5.6.0\", \"le\": \"5.6.11\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.4\"}],\n    }],\n    # The note reads \"FortiOS 5.2.0 to 5.6.10\" (and 6.0.0 to 6.0.4); handmade\n    # started the lower range at 5.6.0 and dropped 5.2.0-5.4.x.\n    (\"FG-IR-19-034\", \"FortiOS\"): [{\n        \"product\": \"FortiOS\",\n        \"ranges\": [{\"ge\": \"5.2.0\", \"le\": \"5.6.10\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.4\"}],\n    }],\n    # The note reads \"6.0.10 and below\" (CNA agrees); handmade wrote le 6.0.11,\n    # pulling the fixed release into the affected range.\n    (\"FG-IR-20-082\", \"FortiOS\"): [{\n        \"product\": \"FortiOS\",\n        \"ranges\": [{\"ge\": \"5.6.0\", \"le\": \"5.6.12\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.10\"},\n                   {\"ge\": \"6.2.0\", \"le\": \"6.2.4\"}, {\"ge\": \"6.4.0\", \"le\": \"6.4.1\"}],\n    }],\n    # Same le 6.0.11 slip as FG-IR-20-082 (\"6.0.10 and below\" per the note).\n    (\"FG-IR-20-103\", \"FortiOS\"): [{\n        \"product\": \"FortiOS\",\n        \"ranges\": [{\"ge\": \"6.0.0\", \"le\": \"6.0.10\"}, {\"ge\": \"6.2.0\", \"le\": \"6.2.4\"},\n                   {\"ge\": \"6.4.0\", \"le\": \"6.4.1\"}],\n    }],\n    # The note reads \"5.6.x and 6.0.x are also impacted\" and the CNA record\n    # enumerates through 6.0.14; handmade stopped the 6.0 train at 6.0.13.\n    (\"FG-IR-20-158\", \"FortiOS\"): [{\n        \"product\": \"FortiOS\", \"versions\": [\"7.0.0\"],\n        \"ranges\": [{\"ge\": \"5.6.0\", \"le\": \"5.6.14\"}, {\"ge\": \"6.0.0\", \"le\": \"6.0.14\"},\n                   {\"ge\": \"6.2.0\", \"le\": \"6.2.9\"}, {\"ge\": \"6.4.0\", \"le\": \"6.4.6\"}],\n    }],\n}\n\n# Reviewed advisories that stay content-only, with the reason. Everything in\n# the gap must end up supplemented or in this list; anything else is reported\n# as unreviewed.\nREVIEWED_CONTENT_ONLY = {\n    \"FG-IR-012-004\": \"hardware appliance models only (no software versions in any source)\",\n    \"FG-IR-012-005\": \"hardware appliance models only (no software versions in any source)\",\n    \"FG-IR-012-006\": \"hardware appliance models only (no software versions in any source)\",\n    \"FG-IR-17-195\": \"hardware appliance models only (no software versions in any source)\",\n    \"FG-IR-17-271\": \"hardware appliance models only (no software versions in any source)\",\n    \"FG-IR-15-001\": \"third-party CVE (glibc GHOST); no Fortinet product versions in any source\",\n    \"FG-IR-15-013\": \"third-party CVE (Logjam); no Fortinet product versions in any source\",\n    \"FG-IR-15-015\": \"third-party CVE (OpenSSL); no Fortinet product versions in any source\",\n    \"FG-IR-15-024\": \"not a product defect (SYN flood filtering explanation)\",\n    \"FG-IR-16-002\": \"third-party CVE (glibc); no Fortinet product versions in any source\",\n    \"FG-IR-16-007\": \"third-party CVE (Badlock/Samba); no Fortinet product versions in any source\",\n    \"FG-IR-16-012\": \"third-party CVE (OpenSSL); no Fortinet product versions in any source\",\n    \"FG-IR-16-022\": \"FortiCloud service-side fix dated by day, not by product version\",\n    \"FG-IR-16-063\": \"third-party CVE (Dirty COW); no Fortinet product versions in any source\",\n    \"FG-IR-16-091\": \"not a product defect (BlackNurse ICMP behaviour)\",\n    \"FG-IR-17-205\": \"third-party CVE (Apache Struts); no Fortinet product versions in any source\",\n    \"FG-IR-17-212\": \"third-party CVE (BlueBorne); no Fortinet product versions in any source\",\n    \"FG-IR-17-249\": \"third-party CVE (Infineon ROCA); no Fortinet product versions in any source\",\n    \"FG-IR-17-251\": \"third-party CVE (Apache Tomcat); no Fortinet product versions in any source\",\n    \"FG-IR-18-046\": \"third-party CVE (AMD CPU flaws); no Fortinet product versions in any source\",\n    \"FG-IR-18-106\": \"no Fortinet device impact established (VPNFilter advisory)\",\n    \"FG-IR-18-112\": \"third-party CVE (BIND); no Fortinet product versions in any source\",\n    \"FG-IR-18-336\": \"third-party CVE (libssh); no Fortinet product versions in any source\",\n    \"FG-IR-19-180\": \"third-party CVE (TCP SACK); no Fortinet product versions in any source\",\n    \"FG-IR-19-222\": \"third-party CVE (VxWorks URGENT/11); no Fortinet product versions in any source\",\n    \"FG-IR-19-224\": \"third-party CVE (Bluetooth KNOB); no Fortinet product versions in any source\",\n    \"FG-IR-19-225\": \"third-party CVE (HTTP/2 flood); no Fortinet product versions in any source\",\n    \"FG-IR-19-292\": \"third-party CVE (CVE-2004-1653 SSH scanning); advisory states most products not impacted\",\n    \"FG-IR-20-035\": \"third-party CVE (Broadcom Kr00k); no Fortinet product versions in any source\",\n    \"FG-IR-20-036\": \"third-party CVE (NTP); no Fortinet product versions in any source\",\n    \"FG-IR-20-104\": \"third-party CVE (Treck Ripple20); no Fortinet product versions in any source\",\n    \"FG-IR-20-128\": \"third-party CVE (Apache httpd); no Fortinet product versions in any source\",\n    \"FG-IR-21-245\": \"third-party CVE (Log4j); no Fortinet product versions in any source\",\n    \"FG-IR-21-253\": \"third-party CVE (Apache httpd); advisory lists only NOT-impacted products\",\n    \"FG-IR-23-277\": \"third-party CVE (Downfall/Zenbleed); informational\",\n    \"FG-IR-23-383\": \"third-party research (TunnelCrack); informational\",\n    \"FG-IR-25-166\": \"third-party CVE (Apache Camel); informational\",\n    \"FG-IR-26-126\": \"covered by the fortinet-csaf dataset\",\n    \"FG-IR-26-139\": \"covered by the fortinet-csaf dataset\",\n    \"FG-IR-26-144\": \"covered by the fortinet-csaf dataset\",\n}\n\n\n# --- cross-validation -------------------------------------------------------\n\ndef affected_predicate(row):\n    \"\"\"row -&gt; f(version_token) -&gt; bool, mirroring the extract-side semantics:\n    exacts match by equality, ranges by numeric comparison, a row with\n    neither matches everything (whole product).\"\"\"\n    def vkey(v):\n        return [int(x) for x in v.split(\".\")]\n    exacts = set(row.get(\"versions\") or [])\n    ranges = row.get(\"ranges\") or []\n    whole = not exacts and not ranges\n\n    def f(v):\n        if whole or v in exacts:\n            return True\n        k = vkey(v)\n        for r in ranges:\n            if r.get(\"ge\") and k &lt; vkey(r[\"ge\"]): continue\n            if r.get(\"gt\") and k &lt;= vkey(r[\"gt\"]): continue\n            if r.get(\"le\") and k &gt; vkey(r[\"le\"]): continue\n            if r.get(\"lt\") and k &gt;= vkey(r[\"lt\"]): continue\n            return True\n        return False\n    return f\n\n\ndef probes_of(row):\n    out = set(row.get(\"versions\") or [])\n    for r in row.get(\"ranges\") or []:\n        out.update(v for v in r.values())\n    return out\n\n\ndef version_level_diffs(aid, hm, cna):\n    \"\"\"Compare handmade vs CNA per product at version level: evaluate every\n    version token either side mentions against both sides' affected\n    predicates and report the disagreements (for human review).\"\"\"\n    out = []\n    hm_by = {r[\"product\"]: r for r in hm}\n    cna_by = {r[\"product\"]: r for r in cna}\n    for name in sorted(set(hm_by) &amp; set(cna_by)):\n        hf, cf = affected_predicate(hm_by[name]), affected_predicate(cna_by[name])\n        diffs = []\n        for v in sorted(probes_of(hm_by[name]) | probes_of(cna_by[name]),\n                        key=lambda s: [int(x) for x in s.split(\".\")]):\n            a, b = hf(v), cf(v)\n            if a != b:\n                diffs.append(f\"{v}: handmade={'Y' if a else 'N'} cna={'Y' if b else 'N'}\")\n        if diffs:\n            out.append(f\"{aid}/{name}: {'; '.join(diffs)}\")\n    return out\n\n\n# --- assembly ---------------------------------------------------------------\n\ndef finish_rows(per_product):\n    rows = []\n    for name in sorted(per_product):\n        e = per_product[name]\n        row = {\"product\": name}\n        exacts = sorted(set(e[\"exacts\"]), key=lambda v: [int(x) for x in v.split(\".\")])\n        ranges = [dict(t) for t in sorted({tuple(sorted(r.items())) for r in e[\"ranges\"]})]\n        if e.get(\"all\") and not exacts and not ranges:\n            pass  # whole-product row: CPE only\n        if exacts:\n            row[\"versions\"] = exacts\n        if ranges:\n            row[\"ranges\"] = ranges\n        if row.get(\"versions\") or row.get(\"ranges\") or e.get(\"all\"):\n            rows.append(row)\n    return rows\n\n\ndef emit_go(supplement, path):\n    \"\"\"Emit the table as a Go literal (supplement_data.go for the cvrf\n    package); supplement.json stays as the tooling-readable byproduct.\"\"\"\n    def q(s):\n        return '\"' + s.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"') + '\"'\n\n    bounds = [(\"ge\", \"GreaterEqual\"), (\"gt\", \"GreaterThan\"),\n              (\"le\", \"LessEqual\"), (\"lt\", \"LessThan\")]\n    with open(path, \"w\") as f:\n        f.write(\"// Code generated by the fortinet-cvrf-supplement generator\"\n                \" (maintainer tooling); DO NOT EDIT by hand.\\n\"\n                \"// See supplement.go for what this table is and how it was\"\n                \" derived.\\n\\npackage cvrf\\n\\n\"\n                \"var supplementTable = map[string][]supplementProduct{\\n\")\n        for aid in sorted(supplement):\n            f.write(f\"\\t{q(aid)}: {{\\n\")\n            for row in supplement[aid]:\n                parts = [f\"Product: {q(row['product'])}\"]\n                if row.get(\"versions\"):\n                    parts.append(\"Versions: []string{\" +\n                                 \", \".join(q(v) for v in row[\"versions\"]) + \"}\")\n                if row.get(\"ranges\"):\n                    rs = []\n                    for r in row[\"ranges\"]:\n                        rs.append(\"{\" + \", \".join(\n                            f\"{go}: {q(r[k])}\" for k, go in bounds if r.get(k)) + \"}\")\n                    parts.append(\"Ranges: []supplementRange{\" + \", \".join(rs) + \"}\")\n                f.write(\"\\t\\t{\" + \", \".join(parts) + \"},\\n\")\n            f.write(\"\\t},\\n\")\n        f.write(\"}\\n\")\n\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--fetch-cves\", action=\"store_true\",\n                    help=\"download cvelistV5 records for the gap CVEs, then exit\")\n    args = ap.parse_args()\n\n    table = load_table()\n    rev = reverse_table(table)\n    hm_paths = load_json_tree(HM)\n    cvrf_paths = load_json_tree(CVRF)\n\n    gap = {}  # aid -&gt; cves\n    for aid, p in sorted(cvrf_paths.items()):\n        doc = json.load(open(p))\n        if cvrf_statuses_empty(doc):\n            gap[aid] = cvrf_cves(doc)\n\n    if args.fetch_cves:\n        os.makedirs(CVELIST, exist_ok=True)\n        cfg = []\n        for cves in gap.values():\n            for cve in cves:\n                dst = os.path.join(CVELIST, cve + \".json\")\n                if os.path.exists(dst):\n                    continue\n                y, n = cve.split(\"-\")[1], int(cve.split(\"-\")[2])\n                url = (f\"https://raw.githubusercontent.com/CVEProject/cvelistV5/\"\n                       f\"main/cves/{y}/{n // 1000}xxx/{cve}.json\")\n                cfg += [f'url = \"{url}\"', f'output = \"{dst}\"']\n        if cfg:\n            subprocess.run([\"curl\", \"-s\", \"--parallel\", \"--parallel-max\", \"16\",\n                            \"--config\", \"-\"], input=\"\\n\".join(cfg),\n                           text=True, check=True)\n        print(f\"cvelist records ready: {len(glob.glob(CVELIST + '/*.json'))}\")\n        return\n\n    problems = []\n    notes = []\n    supplement = {}\n    src_of = {}\n    conflicts = []\n    for aid, cves in gap.items():\n        if aid in MANUAL_OVERRIDES:\n            supplement[aid] = MANUAL_OVERRIDES[aid]\n            src_of[aid] = \"manual\"\n            continue\n        hm_problems = []\n        hm = handmade_rows(aid, hm_paths, rev, table, hm_problems, notes)\n        if aid in HANDMADE_EXCLUDED:\n            hm, hm_problems = [], []\n        cna_problems = []\n        cna = cna_rows(aid, cves, table, cna_problems)\n        # Union at product level: handmade rows win for the products handmade\n        # covers (curated ranges); CNA fills the products handmade misses\n        # (handmade 2018+ often tracks only FortiOS while the CNA record also\n        # lists FortiProxy etc.).\n        hp = {r[\"product\"] for r in hm}\n        if hm and cna:\n            conflicts += version_level_diffs(aid, hm, cna)\n        rows = hm + [r for r in (cna or []) if r[\"product\"] not in hp]\n        rows = [row for r in rows\n                for row in (PRODUCT_ROW_FIXUPS.get((aid, r[\"product\"])) or [r])]\n        if rows:\n            supplement[aid] = rows\n            src_of[aid] = (\"handmade\" if not cna else \"cna\" if not hm else \"handmade+cna\")\n            problems += hm_problems if hm else []\n            problems += cna_problems if cna else []\n            if hm and cna:\n                cp = {r[\"product\"] for r in cna}\n                if not hp &amp; cp:\n                    conflicts.append(\n                        f\"{aid}: DISJOINT handmade={sorted(hp)} cna={sorted(cp)} \u2014 verify against the advisory text\")\n                elif hp != cp:\n                    conflicts.append(\n                        f\"{aid}: products differ handmade={sorted(hp)} cna={sorted(cp)} (union emitted)\")\n        else:\n            problems += hm_problems + cna_problems\n\n    # Validate the assembled table: a lower bound above the upper bound (a\n    # transcription slip like handmade's \"ge 4.4.0, le 4.3.1\") would emit an\n    # unsatisfiable criterion \u2014 a silent detection false negative.\n    def vkey(v):\n        return [int(x) for x in v.split(\".\")]\n    for aid, rows in supplement.items():\n        for r in rows:\n            for rg in r.get(\"ranges\") or []:\n                lo, hi = rg.get(\"ge\") or rg.get(\"gt\"), rg.get(\"le\") or rg.get(\"lt\")\n                if lo and hi and vkey(lo) &gt; vkey(hi):\n                    problems.append(\n                        f\"{aid}: inverted range for {r['product']}: {rg} (lower &gt; upper)\")\n\n    with open(os.path.join(HERE, \"supplement.json\"), \"w\") as f:\n        json.dump(supplement, f, indent=1, sort_keys=True)\n        f.write(\"\\n\")\n\n    emit_go(supplement, os.path.join(HERE, \"supplement_data.go\"))\n\n    by_src = collections.Counter(src_of.values())\n    with open(os.path.join(HERE, \"report.md\"), \"w\") as f:\n        f.write(\"# fortinet-cvrf supplement generation report\\n\\n\")\n        f.write(f\"- statuses-empty CVRF advisories: {len(gap)}\\n\")\n        f.write(f\"- supplemented: {len(supplement)} (handmade {by_src['handmade']},\"\n                f\" cna {by_src['cna']}, handmade+cna {by_src['handmade+cna']},\"\n                f\" manual {by_src['manual']})\\n\")\n        f.write(f\"- reviewed content-only: {len(set(gap) &amp; set(REVIEWED_CONTENT_ONLY))}\\n\")\n        unreviewed = sorted(set(gap) - set(supplement) - set(REVIEWED_CONTENT_ONLY))\n        f.write(f\"- UNREVIEWED (must be empty): {len(unreviewed)}\\n\\n\")\n        f.write(\"## unreviewed advisories\\n\\n\")\n        for aid in unreviewed:\n            f.write(f\"- {aid} (CVEs: {', '.join(gap[aid]) or 'none'})\\n\")\n        f.write(\"\\n## reviewed content-only advisories\\n\\n\")\n        for aid in sorted(set(gap) &amp; set(REVIEWED_CONTENT_ONLY)):\n            f.write(f\"- {aid}: {REVIEWED_CONTENT_ONLY[aid]}\\n\")\n        f.write(\"\\n## handmade vs CNA product-set diffs\\n\\n\")\n        for c in conflicts:\n            f.write(f\"- {c}\\n\")\n        f.write(\"\\n## problems\\n\\n\")\n        for p in sorted(set(problems)):\n            f.write(f\"- {p}\\n\")\n        f.write(\"\\n## notes (deliberate skips)\\n\\n\")\n        for p in sorted(set(notes)):\n            f.write(f\"- {p}\\n\")\n\n    print(f\"gap={len(gap)} supplemented={len(supplement)} \"\n          f\"(hm={by_src['handmade']} cna={by_src['cna']}) \"\n          f\"problems={len(set(problems))} conflicts={len(conflicts)}\")\n    print(f\"wrote {os.path.join(HERE, 'supplement.json')} and report.md\")\n\n\nif __name__ == \"__main__\":\n    main()\n", "creation_timestamp": "2026-07-22T09:15:44.016885Z"}