{"uuid": "70d3d0c4-5525-4767-a13a-1ba4466df3d3", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2024-1234", "type": "seen", "source": "https://gist.github.com/yannhowe/bc79334e9ba4f17106e2a63e09047707", "content": "#!/usr/bin/env python3\n\"\"\"\nFalcon Container Image Assessment Report\nExports ALL image fields to CSV - bypasses the 10-column UI limit.\n\nFixes:\n  - UI only shows 10 vulnerability columns \u2192 exports all 25+ fields\n  - Can't filter by last scanned date \u2192 use --last-scanned-after / --before\n  - Missing fields (container_id, registry, tag, image_id) \u2192 all included\n  - Build labels included in output (note: FQL filter not supported by API)\n\nUsage:\n  # All images, full CSV\n  python3 falcon-image-assessment-report.py\n\n  # Filter by registry (Azure Container Registry)\n  python3 falcon-image-assessment-report.py --registry myregistry.azurecr.io\n\n  # Images with critical vulnerabilities\n  python3 falcon-image-assessment-report.py --severity critical\n\n  # Scanned in last 7 days\n  python3 falcon-image-assessment-report.py --last-scanned-after 2024-01-01\n\n  # Images affected by a specific CVE\n  python3 falcon-image-assessment-report.py --cve CVE-2024-1234\n\n  # Only running containers\n  python3 falcon-image-assessment-report.py --running-only\n\n  # Expand to one row per CVE (for per-vulnerability filtering)\n  python3 falcon-image-assessment-report.py --expand-vulns --severity critical\n\n  # Save output\n  python3 falcon-image-assessment-report.py --output /tmp/images.csv\n\"\"\"\n\nimport sys\nimport os\nimport json\nimport subprocess\nimport requests\nimport csv\nimport argparse\nfrom datetime import datetime, timezone, timedelta\nfrom typing import Optional\n\n\n# \u2500\u2500 Auth boilerplate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef get_falcon_profile() -&gt; str:\n    profile = os.getenv('FALCON_PROFILE')\n    if profile:\n        return profile\n    for path in ['.claude/memory/active-cid.txt',\n                 os.path.expanduser('~/.claude/projects/-Users-ykwan-Documents-code-knowledgebase/memory/active-cid.txt')]:\n        try:\n            with open(path) as f:\n                for line in f:\n                    if line.startswith('profile='):\n                        return line.strip().split('=', 1)[1]\n        except FileNotFoundError:\n            continue\n    return 'default'\n\n\ndef get_keychain_password(service: str, account: str, profile: Optional[str] = None) -&gt; Optional[str]:\n    if profile is None:\n        profile = get_falcon_profile()\n    try:\n        result = subprocess.run(\n            ['security', 'find-generic-password', '-s', service, '-a', profile, '-w'],\n            capture_output=True, text=True, check=True)\n        return result.stdout.strip()\n    except subprocess.CalledProcessError:\n        pass\n    if profile == 'default':\n        try:\n            result = subprocess.run(\n                ['security', 'find-generic-password', '-s', 'crowdstrike-falcon-api', '-a', account, '-w'],\n                capture_output=True, text=True, check=True)\n            return result.stdout.strip()\n        except subprocess.CalledProcessError:\n            pass\n    return None\n\n\ndef get_oauth_token(base_url=\"https://api.crowdstrike.com\", profile=None):\n    if profile is None:\n        profile = get_falcon_profile()\n    client_id = get_keychain_password(\"falcon-client-id\", \"client-id\", profile)\n    client_secret = get_keychain_password(\"falcon-client-secret\", \"client-secret\", profile)\n    if not client_id or not client_secret:\n        print(f\"Credentials not found for profile: {profile}\")\n        print(f\"Run: /cid add {profile}\")\n        sys.exit(1)\n    url = f\"{base_url}/oauth2/token\"\n    data = {\"client_id\": client_id, \"client_secret\": client_secret}\n    resp = requests.post(url, headers={\"Content-Type\": \"application/x-www-form-urlencoded\"}, data=data)\n    if resp.status_code != 201:\n        print(f\"Auth failed: {resp.status_code} {resp.text}\")\n        sys.exit(1)\n    return resp.json()[\"access_token\"]\n\n\n# \u2500\u2500 API helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef fetch_images_page(token, fql_filter, offset, limit, expand_vulns, base_url):\n    \"\"\"Single page from /container-security/combined/images/export/v1\"\"\"\n    url = f\"{base_url}/container-security/combined/images/export/v1\"\n    params = {\n        \"limit\": limit,\n        \"offset\": offset,\n        \"expand_vulnerabilities\": \"true\" if expand_vulns else \"false\",\n        \"expand_detections\": \"false\",\n        \"sort\": \"last_seen.desc\",\n    }\n    if fql_filter:\n        params[\"filter\"] = fql_filter\n    headers = {\"Authorization\": f\"Bearer {token}\"}\n    resp = requests.get(url, headers=headers, params=params)\n    if resp.status_code != 200:\n        print(f\"  API error {resp.status_code}: {resp.text[:300]}\")\n        return [], 0\n    body = resp.json()\n    resources = body.get(\"resources\") or []\n    total = body.get(\"meta\", {}).get(\"pagination\", {}).get(\"total\", len(resources))\n    return resources, total\n\n\ndef fetch_all_images(token, fql_filter, expand_vulns, base_url, page_size=500):\n    \"\"\"Paginate through all matching images.\"\"\"\n    all_images = []\n    offset = 0\n    total = None\n    while True:\n        batch, total = fetch_images_page(token, fql_filter, offset, page_size, expand_vulns, base_url)\n        if not batch:\n            break\n        all_images.extend(batch)\n        print(f\"  Fetched {len(all_images)} / {total}\", end=\"\\r\")\n        if len(all_images) &gt;= total or len(batch) &lt; page_size:\n            break\n        offset += page_size\n    print()\n    return all_images, total\n\n\n# \u2500\u2500 Flattening \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef safe_get(d, *keys, default=\"\"):\n    \"\"\"Nested dict get with default.\"\"\"\n    for k in keys:\n        if not isinstance(d, dict):\n            return default\n        d = d.get(k, default)\n    return d if d != \"\" or default == \"\" else default\n\n\ndef flatten_image_base(img):\n    \"\"\"Extract all standard image fields into a flat dict.\"\"\"\n    # Vulnerability counts - API may return nested or flat depending on endpoint\n    vuln = img.get(\"vulnerabilities\") or {}\n    if isinstance(vuln, list):\n        # Expanded mode - list of CVE objects; summarise counts\n        sev_counts = {\"critical\": 0, \"high\": 0, \"medium\": 0, \"low\": 0, \"negligible\": 0}\n        for v in vuln:\n            s = (v.get(\"severity\") or \"\").lower()\n            if s in sev_counts:\n                sev_counts[s] += 1\n        vuln_summary = sev_counts\n        vuln_list = vuln\n    else:\n        vuln_summary = vuln\n        vuln_list = []\n\n    detection = img.get(\"detections\") or {}\n\n    # Build labels - present if API returns them; not FQL-filterable today\n    labels = img.get(\"labels\") or img.get(\"build_labels\") or {}\n    labels_str = \"; \".join(f\"{k}={v}\" for k, v in labels.items()) if isinstance(labels, dict) else str(labels)\n\n    row = {\n        # Identity\n        \"image_id\":                img.get(\"id\") or img.get(\"image_id\", \"\"),\n        \"image_digest\":            img.get(\"image_digest\", \"\"),\n        \"registry\":                img.get(\"registry\", \"\"),\n        \"repository\":              img.get(\"repository\", \"\"),\n        \"tag\":                     img.get(\"tag\", \"\"),\n        \"source\":                  img.get(\"source\", \"\"),\n        # Properties\n        \"arch\":                    img.get(\"arch\", \"\"),\n        \"base_os\":                 img.get(\"base_os\", \"\"),\n        \"multi_arch\":              img.get(\"multi_arch\", \"\"),\n        # Runtime\n        \"container_id\":            img.get(\"container_id\", \"\"),\n        \"container_running_status\": img.get(\"container_running_status\", \"\"),\n        # Timestamps\n        \"first_seen\":              img.get(\"first_seen\", \"\"),\n        \"last_seen\":               img.get(\"last_seen\", \"\"),\n        # Scores\n        \"cps_rating\":              img.get(\"highest_cps_current_rating\", \"\"),\n        # Vulnerabilities\n        \"vuln_critical\":           vuln_summary.get(\"critical\", 0),\n        \"vuln_high\":               vuln_summary.get(\"high\", 0),\n        \"vuln_medium\":             vuln_summary.get(\"medium\", 0),\n        \"vuln_low\":                vuln_summary.get(\"low\", 0),\n        \"vuln_negligible\":         vuln_summary.get(\"negligible\", 0),\n        \"vuln_total\":              img.get(\"vulnerability_count\", sum(vuln_summary.get(s, 0) for s in [\"critical\",\"high\",\"medium\",\"low\",\"negligible\"])),\n        \"highest_vuln_severity\":   img.get(\"highest_vulnerability_severity\", \"\"),\n        # Detections\n        \"detection_count\":         img.get(\"detection_count\", safe_get(detection, \"total\")),\n        \"highest_detection_severity\": img.get(\"highest_detection_severity\", \"\"),\n        # Packages / layers\n        \"package_count\":           img.get(\"packages\", \"\"),\n        \"layers_with_vulns\":       img.get(\"layers_with_vulnerabilities\", \"\"),\n        # Build metadata\n        \"build_labels\":            labels_str,\n    }\n    return row, vuln_list\n\n\ndef expand_vuln_rows(base_row, vuln_list):\n    \"\"\"Return one row per CVE for expanded mode.\"\"\"\n    if not vuln_list:\n        return [base_row]\n    rows = []\n    for v in vuln_list:\n        row = dict(base_row)\n        row[\"cve_id\"] = v.get(\"cve_id\", \"\")\n        row[\"cve_severity\"] = v.get(\"severity\", \"\")\n        row[\"cvss_score\"] = v.get(\"cvss_score\", \"\")\n        row[\"cve_description\"] = v.get(\"description\", \"\")\n        row[\"fix_status\"] = v.get(\"fix_status\", \"\")\n        row[\"remediation\"] = v.get(\"remediation\", \"\")\n        row[\"exploited_status\"] = v.get(\"exploited_status\", \"\")\n        row[\"package_name\"] = v.get(\"package_name\", \"\")\n        row[\"package_version\"] = v.get(\"package_version\", \"\")\n        row[\"package_path\"] = v.get(\"package_path\", \"\")\n        rows.append(row)\n    return rows\n\n\n# \u2500\u2500 FQL filter builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef build_fql(args):\n    parts = []\n    if args.registry:\n        parts.append(f\"registry:'{args.registry}'\")\n    if args.repository:\n        parts.append(f\"repository:'{args.repository}'\")\n    if args.tag:\n        parts.append(f\"tag:'{args.tag}'\")\n    if args.severity:\n        parts.append(f\"vulnerability_severity:'{args.severity}'\")\n    if args.cve:\n        parts.append(f\"cve_id:'{args.cve}'\")\n    if args.running_only:\n        parts.append(\"container_running_status:true\")\n    if args.last_scanned_after:\n        ts = args.last_scanned_after\n        if len(ts) == 10:  # date only \u2192 add time\n            ts += \"T00:00:00Z\"\n        parts.append(f\"last_seen:&gt;='{ts}'\")\n    if args.last_scanned_before:\n        ts = args.last_scanned_before\n        if len(ts) == 10:\n            ts += \"T23:59:59Z\"\n        parts.append(f\"last_seen:&lt;='{ts}'\")\n    return \"+\".join(parts) if parts else None\n\n\n# \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Export Falcon Container Image Assessment data to CSV with ALL fields.\")\n    parser.add_argument(\"--profile\", help=\"CID profile (default: active profile)\")\n    parser.add_argument(\"--registry\", help=\"Filter by registry (e.g. myregistry.azurecr.io)\")\n    parser.add_argument(\"--repository\", help=\"Filter by repository name\")\n    parser.add_argument(\"--tag\", help=\"Filter by image tag\")\n    parser.add_argument(\"--severity\", choices=[\"critical\",\"high\",\"medium\",\"low\"],\n                        help=\"Filter by highest vulnerability severity\")\n    parser.add_argument(\"--cve\", help=\"Filter images affected by a specific CVE\")\n    parser.add_argument(\"--running-only\", action=\"store_true\",\n                        help=\"Only include currently running containers\")\n    parser.add_argument(\"--last-scanned-after\", metavar=\"DATE\",\n                        help=\"Only images scanned after this date (YYYY-MM-DD or ISO8601)\")\n    parser.add_argument(\"--last-scanned-before\", metavar=\"DATE\",\n                        help=\"Only images scanned before this date (YYYY-MM-DD or ISO8601)\")\n    parser.add_argument(\"--expand-vulns\", action=\"store_true\",\n                        help=\"One row per CVE (instead of one row per image)\")\n    parser.add_argument(\"--output\", \"-o\", default=\"-\",\n                        help=\"Output CSV file path (default: stdout)\")\n    parser.add_argument(\"--limit\", type=int, default=5000,\n                        help=\"Max images to fetch (default: 5000)\")\n    args = parser.parse_args()\n\n    profile = args.profile or get_falcon_profile()\n    region = get_keychain_password(\"falcon-cloud-region\", \"region\", profile) or \"us-1\"\n    base_url = \"https://api.crowdstrike.com\" if region == \"us-1\" else f\"https://api.{region}.crowdstrike.com\"\n\n    print(f\"=== Falcon Image Assessment Report ===\", file=sys.stderr)\n    print(f\"Profile: {profile}  Region: {region}\", file=sys.stderr)\n\n    token = get_oauth_token(base_url, profile=profile)\n    print(\"\u2713 Authenticated\", file=sys.stderr)\n\n    fql = build_fql(args)\n    if fql:\n        print(f\"Filter: {fql}\", file=sys.stderr)\n\n    print(\"Fetching images...\", file=sys.stderr)\n    images, total = fetch_all_images(token, fql, args.expand_vulns, base_url,\n                                      page_size=min(500, args.limit))\n    if total and len(images) &lt; total:\n        print(f\"\u26a0  Fetched {len(images)} of {total} total (increase --limit to get all)\", file=sys.stderr)\n    print(f\"\u2713 {len(images)} images retrieved\", file=sys.stderr)\n\n    if not images:\n        print(\"No images found matching filters.\", file=sys.stderr)\n        sys.exit(0)\n\n    # Build rows\n    all_rows = []\n    for img in images:\n        base_row, vuln_list = flatten_image_base(img)\n        if args.expand_vulns:\n            all_rows.extend(expand_vuln_rows(base_row, vuln_list))\n        else:\n            all_rows.append(base_row)\n\n    # Write CSV\n    fieldnames = list(all_rows[0].keys())\n\n    out = open(args.output, \"w\", newline=\"\") if args.output != \"-\" else sys.stdout\n    writer = csv.DictWriter(out, fieldnames=fieldnames, extrasaction=\"ignore\")\n    writer.writeheader()\n    writer.writerows(all_rows)\n    if args.output != \"-\":\n        out.close()\n        print(f\"\u2713 Written to {args.output}  ({len(all_rows)} rows)\", file=sys.stderr)\n    else:\n        print(f\"\\n\u2713 {len(all_rows)} rows written\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n    main()\n\n\n#!/usr/bin/env python3\n\"\"\"\nFalcon Package Vulnerability Report - Exploded CVE Format\nOne row per (package \u00d7 CVE) combination. Fixes the \"combined fields\" CSV problem.\n\nFixes:\n  - Package Vulnerabilities CSV combines all CVE IDs into one field \u2192 each CVE = own row\n  - Can't filter by a specific CVE to see every affected package \u2192 use --cve\n  - Missing image/container context per package \u2192 includes image list per package+CVE\n  - CVE descriptions and remediations combined \u2192 each in its own column\n\nUsage:\n  # All packages with vulnerabilities\n  python3 falcon-package-cve-report.py\n\n  # See every package affected by a specific CVE\n  python3 falcon-package-cve-report.py --cve CVE-2024-1234\n\n  # Critical and high only\n  python3 falcon-package-cve-report.py --severity critical\n  python3 falcon-package-cve-report.py --severity high\n\n  # Fixable vulnerabilities only\n  python3 falcon-package-cve-report.py --fix-available\n\n  # Filter by registry (to scope to Azure Container Apps images)\n  python3 falcon-package-cve-report.py --registry myregistry.azurecr.io\n\n  # Save output\n  python3 falcon-package-cve-report.py --cve CVE-2024-1234 --output /tmp/cve-impact.csv\n\"\"\"\n\nimport sys\nimport os\nimport json\nimport subprocess\nimport requests\nimport csv\nimport argparse\nfrom typing import Optional\n\n\n# \u2500\u2500 Auth boilerplate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef get_falcon_profile() -&gt; str:\n    profile = os.getenv('FALCON_PROFILE')\n    if profile:\n        return profile\n    for path in ['.claude/memory/active-cid.txt',\n                 os.path.expanduser('~/.claude/projects/-Users-ykwan-Documents-code-knowledgebase/memory/active-cid.txt')]:\n        try:\n            with open(path) as f:\n                for line in f:\n                    if line.startswith('profile='):\n                        return line.strip().split('=', 1)[1]\n        except FileNotFoundError:\n            continue\n    return 'default'\n\n\ndef get_keychain_password(service: str, account: str, profile: Optional[str] = None) -&gt; Optional[str]:\n    if profile is None:\n        profile = get_falcon_profile()\n    try:\n        result = subprocess.run(\n            ['security', 'find-generic-password', '-s', service, '-a', profile, '-w'],\n            capture_output=True, text=True, check=True)\n        return result.stdout.strip()\n    except subprocess.CalledProcessError:\n        pass\n    if profile == 'default':\n        try:\n            result = subprocess.run(\n                ['security', 'find-generic-password', '-s', 'crowdstrike-falcon-api', '-a', account, '-w'],\n                capture_output=True, text=True, check=True)\n            return result.stdout.strip()\n        except subprocess.CalledProcessError:\n            pass\n    return None\n\n\ndef get_oauth_token(base_url=\"https://api.crowdstrike.com\", profile=None):\n    if profile is None:\n        profile = get_falcon_profile()\n    client_id = get_keychain_password(\"falcon-client-id\", \"client-id\", profile)\n    client_secret = get_keychain_password(\"falcon-client-secret\", \"client-secret\", profile)\n    if not client_id or not client_secret:\n        print(f\"Credentials not found for profile: {profile}\")\n        print(f\"Run: /cid add {profile}\")\n        sys.exit(1)\n    url = f\"{base_url}/oauth2/token\"\n    data = {\"client_id\": client_id, \"client_secret\": client_secret}\n    resp = requests.post(url, headers={\"Content-Type\": \"application/x-www-form-urlencoded\"}, data=data)\n    if resp.status_code != 201:\n        print(f\"Auth failed: {resp.status_code} {resp.text}\")\n        sys.exit(1)\n    return resp.json()[\"access_token\"]\n\n\n# \u2500\u2500 API: Package export (with embedded CVEs) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef fetch_packages_page(token, fql_filter, offset, limit, base_url):\n    \"\"\"GET /container-security/combined/packages-export/v1\"\"\"\n    url = f\"{base_url}/container-security/combined/packages-export/v1\"\n    params = {\"limit\": limit, \"offset\": offset}\n    if fql_filter:\n        params[\"filter\"] = fql_filter\n    headers = {\"Authorization\": f\"Bearer {token}\"}\n    resp = requests.get(url, headers=headers, params=params)\n    if resp.status_code != 200:\n        print(f\"  API error {resp.status_code}: {resp.text[:400]}\", file=sys.stderr)\n        return [], 0\n    body = resp.json()\n    resources = body.get(\"resources\") or []\n    total = body.get(\"meta\", {}).get(\"pagination\", {}).get(\"total\", len(resources))\n    return resources, total\n\n\ndef fetch_all_packages(token, fql_filter, base_url, page_size=500):\n    all_pkgs = []\n    offset = 0\n    total = None\n    while True:\n        batch, total = fetch_packages_page(token, fql_filter, offset, page_size, base_url)\n        if not batch:\n            break\n        all_pkgs.extend(batch)\n        print(f\"  Fetched {len(all_pkgs)} / {total}\", end=\"\\r\", file=sys.stderr)\n        if len(all_pkgs) &gt;= total or len(batch) &lt; page_size:\n            break\n        offset += page_size\n    print(file=sys.stderr)\n    return all_pkgs, total\n\n\n# \u2500\u2500 API: CVE-specific vulnerability info (images + packages per CVE) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef fetch_vuln_info(token, cve_id, base_url, limit=500):\n    \"\"\"\n    GET /container-security/combined/vulnerabilities-info/v1\n    Returns package + image data for a single CVE.\n    Use this for --cve mode to get the most complete picture.\n    \"\"\"\n    url = f\"{base_url}/container-security/combined/vulnerabilities-info/v1\"\n    all_results = []\n    offset = 0\n    while True:\n        params = {\"cve_id\": cve_id, \"limit\": limit, \"offset\": offset}\n        headers = {\"Authorization\": f\"Bearer {token}\"}\n        resp = requests.get(url, headers=headers, params=params)\n        if resp.status_code != 200:\n            print(f\"  vuln-info error {resp.status_code}: {resp.text[:300]}\", file=sys.stderr)\n            break\n        body = resp.json()\n        batch = body.get(\"resources\") or []\n        all_results.extend(batch)\n        total = body.get(\"meta\", {}).get(\"pagination\", {}).get(\"total\", len(batch))\n        if len(all_results) &gt;= total or len(batch) &lt; limit:\n            break\n        offset += limit\n    return all_results\n\n\n# \u2500\u2500 Row builders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef explode_package_to_rows(pkg):\n    \"\"\"\n    Convert one package record (with embedded vulnerabilities list) into\n    N rows, one per CVE.\n    \"\"\"\n    # Package-level fields\n    base = {\n        \"package_name\":        pkg.get(\"package_name\") or pkg.get(\"name\", \"\"),\n        \"package_version\":     pkg.get(\"package_version\") or pkg.get(\"version\", \"\"),\n        \"package_type\":        pkg.get(\"type\", \"\"),\n        \"package_path\":        pkg.get(\"package_path\") or pkg.get(\"path\", \"\"),\n        \"license\":             pkg.get(\"license\", \"\"),\n        \"fix_status\":          pkg.get(\"fix_status\", \"\"),\n        \"running_images_count\": pkg.get(\"running_images_count\", \"\"),\n        \"all_images_count\":    pkg.get(\"images_count\", \"\"),\n        # Image context - API may return a list of images for this package\n        \"affected_registries\": \"; \".join(set(\n            i.get(\"registry\", \"\") for i in (pkg.get(\"images\") or []) if i.get(\"registry\")\n        )),\n        \"affected_repositories\": \"; \".join(set(\n            i.get(\"repository\", \"\") for i in (pkg.get(\"images\") or []) if i.get(\"repository\")\n        )),\n        \"affected_image_ids\":  \"; \".join(\n            i.get(\"image_id\") or i.get(\"id\", \"\") for i in (pkg.get(\"images\") or [])[:20]\n        ),\n        \"affected_image_tags\": \"; \".join(\n            f\"{i.get('repository','')}:{i.get('tag','')}\" for i in (pkg.get(\"images\") or [])[:20]\n        ),\n        \"affected_container_ids\": \"; \".join(\n            i.get(\"container_id\", \"\") for i in (pkg.get(\"images\") or []) if i.get(\"container_id\")\n        ),\n    }\n\n    vulns = pkg.get(\"vulnerabilities\") or pkg.get(\"cve_ids\") or []\n\n    if not vulns:\n        # No CVE data embedded - return one row with empty CVE fields\n        row = dict(base)\n        row.update({\n            \"cve_id\": \"\",\n            \"severity\": \"\",\n            \"cvss_score\": \"\",\n            \"description\": \"\",\n            \"remediation\": \"\",\n            \"fix_available\": \"\",\n            \"exploited_status\": \"\",\n            \"is_zero_day\": \"\",\n            \"published_date\": \"\",\n        })\n        return [row]\n\n    rows = []\n    for v in vulns:\n        # vulns may be strings (CVE IDs) or dicts depending on endpoint\n        if isinstance(v, str):\n            row = dict(base)\n            row.update({\n                \"cve_id\": v,\n                \"severity\": \"\",\n                \"cvss_score\": \"\",\n                \"description\": \"\",\n                \"remediation\": \"\",\n                \"fix_available\": \"\",\n                \"exploited_status\": \"\",\n                \"is_zero_day\": \"\",\n                \"published_date\": \"\",\n            })\n        else:\n            row = dict(base)\n            row.update({\n                \"cve_id\":          v.get(\"cve_id\", \"\"),\n                \"severity\":        v.get(\"severity\", \"\"),\n                \"cvss_score\":      v.get(\"cvss_score\", \"\"),\n                \"description\":     v.get(\"description\", \"\"),\n                \"remediation\":     v.get(\"remediation\", \"\"),\n                \"fix_available\":   v.get(\"fix_status\", \"\") or base[\"fix_status\"],\n                \"exploited_status\": v.get(\"exploited_status\", \"\"),\n                \"is_zero_day\":     v.get(\"is_zero_day\", \"\"),\n                \"published_date\":  v.get(\"published_date\", \"\"),\n            })\n        rows.append(row)\n    return rows\n\n\ndef rows_from_vuln_info(cve_id, resources):\n    \"\"\"\n    Build rows from /vulnerabilities-info/v1 response.\n    Each resource is a package with embedded image list.\n    \"\"\"\n    rows = []\n    for r in resources:\n        row = {\n            \"cve_id\":          cve_id,\n            \"severity\":        r.get(\"severity\", \"\"),\n            \"cvss_score\":      r.get(\"cvss_score\", \"\"),\n            \"description\":     r.get(\"description\", \"\"),\n            \"remediation\":     r.get(\"remediation\", \"\"),\n            \"fix_available\":   r.get(\"fix_status\", \"\"),\n            \"exploited_status\": r.get(\"exploited_status\", \"\"),\n            \"is_zero_day\":     r.get(\"is_zero_day\", \"\"),\n            \"published_date\":  r.get(\"published_date\", \"\"),\n            \"package_name\":    r.get(\"package_name\", \"\"),\n            \"package_version\": r.get(\"package_version\", \"\"),\n            \"package_type\":    r.get(\"package_type\") or r.get(\"type\", \"\"),\n            \"package_path\":    r.get(\"package_path\", \"\"),\n            \"license\":         r.get(\"license\", \"\"),\n            \"running_images_count\": r.get(\"running_images_count\", \"\"),\n            \"all_images_count\": r.get(\"images_count\", \"\"),\n            # Affected images list\n            \"affected_registries\":    \"; \".join(set(\n                i.get(\"registry\", \"\") for i in (r.get(\"images\") or []) if i.get(\"registry\")\n            )),\n            \"affected_repositories\":  \"; \".join(set(\n                i.get(\"repository\", \"\") for i in (r.get(\"images\") or []) if i.get(\"repository\")\n            )),\n            \"affected_image_tags\":    \"; \".join(\n                f\"{i.get('repository','')}:{i.get('tag','')}\" for i in (r.get(\"images\") or [])[:30]\n            ),\n            \"affected_image_ids\":     \"; \".join(\n                i.get(\"image_id\") or i.get(\"id\", \"\") for i in (r.get(\"images\") or [])[:30]\n            ),\n            \"affected_container_ids\": \"; \".join(\n                i.get(\"container_id\", \"\") for i in (r.get(\"images\") or []) if i.get(\"container_id\")\n            ),\n        }\n        rows.append(row)\n    return rows\n\n\n# \u2500\u2500 FQL builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef build_fql(args):\n    parts = []\n    if args.severity:\n        parts.append(f\"severity:'{args.severity}'\")\n    if args.registry:\n        # package API filters by image metadata\n        parts.append(f\"registry:'{args.registry}'\")\n    if args.cve:\n        parts.append(f\"cveid:'{args.cve}'\")\n    if args.fix_available:\n        parts.append(\"fix_status:'TRUE'\")\n    return \"+\".join(parts) if parts else None\n\n\n# \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Export package vulnerabilities as one row per CVE (fixes combined-fields CSV).\")\n    parser.add_argument(\"--profile\", help=\"CID profile\")\n    parser.add_argument(\"--cve\", help=\"Show all packages affected by this CVE (e.g. CVE-2024-1234)\")\n    parser.add_argument(\"--severity\", choices=[\"critical\",\"high\",\"medium\",\"low\"],\n                        help=\"Filter by vulnerability severity\")\n    parser.add_argument(\"--registry\", help=\"Filter by image registry\")\n    parser.add_argument(\"--fix-available\", action=\"store_true\",\n                        help=\"Only include vulnerabilities with a fix available\")\n    parser.add_argument(\"--output\", \"-o\", default=\"-\",\n                        help=\"Output CSV path (default: stdout)\")\n    parser.add_argument(\"--limit\", type=int, default=5000,\n                        help=\"Max packages to fetch (default: 5000)\")\n    args = parser.parse_args()\n\n    profile = args.profile or get_falcon_profile()\n    region = get_keychain_password(\"falcon-cloud-region\", \"region\", profile) or \"us-1\"\n    base_url = \"https://api.crowdstrike.com\" if region == \"us-1\" else f\"https://api.{region}.crowdstrike.com\"\n\n    print(\"=== Falcon Package CVE Report ===\", file=sys.stderr)\n    print(f\"Profile: {profile}  Region: {region}\", file=sys.stderr)\n\n    token = get_oauth_token(base_url, profile=profile)\n    print(\"\u2713 Authenticated\", file=sys.stderr)\n\n    all_rows = []\n\n    if args.cve:\n        # CVE-first mode: use vulnerabilities-info endpoint for richest data\n        print(f\"Fetching all packages affected by {args.cve}...\", file=sys.stderr)\n        resources = fetch_vuln_info(token, args.cve, base_url)\n        print(f\"\u2713 {len(resources)} package records found\", file=sys.stderr)\n        all_rows = rows_from_vuln_info(args.cve, resources)\n    else:\n        # Package-first mode: dump all packages with exploded CVEs\n        fql = build_fql(args)\n        if fql:\n            print(f\"Filter: {fql}\", file=sys.stderr)\n        print(\"Fetching packages...\", file=sys.stderr)\n        packages, total = fetch_all_packages(token, fql, base_url,\n                                              page_size=min(500, args.limit))\n        if total and len(packages) &lt; total:\n            print(f\"\u26a0  Fetched {len(packages)} of {total} total\", file=sys.stderr)\n        print(f\"\u2713 {len(packages)} packages retrieved, exploding CVEs...\", file=sys.stderr)\n        for pkg in packages:\n            all_rows.extend(explode_package_to_rows(pkg))\n\n    if not all_rows:\n        print(\"No results found.\", file=sys.stderr)\n        sys.exit(0)\n\n    # Apply post-filter for severity (can't always push to FQL in package endpoint)\n    if args.severity and not args.cve:\n        before = len(all_rows)\n        all_rows = [r for r in all_rows if r.get(\"severity\", \"\").lower() == args.severity]\n        print(f\"  Severity filter: {before} \u2192 {len(all_rows)} rows\", file=sys.stderr)\n\n    if args.fix_available:\n        before = len(all_rows)\n        all_rows = [r for r in all_rows\n                    if str(r.get(\"fix_available\", \"\")).upper() in (\"TRUE\", \"YES\", \"1\")]\n        print(f\"  Fix-available filter: {before} \u2192 {len(all_rows)} rows\", file=sys.stderr)\n\n    fieldnames = list(all_rows[0].keys())\n    out = open(args.output, \"w\", newline=\"\") if args.output != \"-\" else sys.stdout\n    writer = csv.DictWriter(out, fieldnames=fieldnames, extrasaction=\"ignore\")\n    writer.writeheader()\n    writer.writerows(all_rows)\n    if args.output != \"-\":\n        out.close()\n        print(f\"\u2713 Written to {args.output}  ({len(all_rows)} rows)\", file=sys.stderr)\n    else:\n        print(f\"\\n\u2713 {len(all_rows)} rows written\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n    main()\n", "creation_timestamp": "2026-05-11T14:19:10.000000Z"}