get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/1.0/patches/2197823/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2197823,
    "url": "http://patchwork.ozlabs.org/api/1.0/patches/2197823/?format=api",
    "project": {
        "id": 27,
        "url": "http://patchwork.ozlabs.org/api/1.0/projects/27/?format=api",
        "name": "Buildroot development",
        "link_name": "buildroot",
        "list_id": "buildroot.buildroot.org",
        "list_email": "buildroot@buildroot.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260218163205.1639035-3-fabien.lehoussel@smile.fr>",
    "date": "2026-02-18T16:32:04",
    "name": "[2/2] support/scripts: add filter-kernel-cve script for Linux kernel CVE analysis",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "9d4add0ff9fb234da4e7c20f450ce4c02fa16140",
    "submitter": {
        "id": 91059,
        "url": "http://patchwork.ozlabs.org/api/1.0/people/91059/?format=api",
        "name": "Fabien LEHOUSSEL",
        "email": "fabien.lehoussel@smile.fr"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/buildroot/patch/20260218163205.1639035-3-fabien.lehoussel@smile.fr/mbox/",
    "series": [
        {
            "id": 492573,
            "url": "http://patchwork.ozlabs.org/api/1.0/series/492573/?format=api",
            "date": "2026-02-18T16:32:03",
            "name": "Linux kernel CVE filtering improvements",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/492573/mbox/"
        }
    ],
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2197823/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<buildroot-bounces@buildroot.org>",
        "X-Original-To": [
            "incoming-buildroot@patchwork.ozlabs.org",
            "buildroot@buildroot.org"
        ],
        "Delivered-To": [
            "patchwork-incoming-buildroot@legolas.ozlabs.org",
            "buildroot@buildroot.org"
        ],
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=buildroot.org header.i=@buildroot.org\n header.a=rsa-sha256 header.s=default header.b=iAOwI8PP;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=buildroot.org\n (client-ip=2605:bc80:3010::136; helo=smtp3.osuosl.org;\n envelope-from=buildroot-bounces@buildroot.org; receiver=patchwork.ozlabs.org)"
        ],
        "Received": [
            "from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fGMVY1fT9z1xvq\n\tfor <incoming-buildroot@patchwork.ozlabs.org>;\n Thu, 19 Feb 2026 03:32:21 +1100 (AEDT)",
            "from localhost (localhost [127.0.0.1])\n\tby smtp3.osuosl.org (Postfix) with ESMTP id C309C60904;\n\tWed, 18 Feb 2026 16:32:19 +0000 (UTC)",
            "from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id 0Q_UzOJkJJLJ; Wed, 18 Feb 2026 16:32:17 +0000 (UTC)",
            "from lists1.osuosl.org (lists1.osuosl.org [140.211.166.142])\n\tby smtp3.osuosl.org (Postfix) with ESMTP id A690A60A4E;\n\tWed, 18 Feb 2026 16:32:17 +0000 (UTC)",
            "from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136])\n by lists1.osuosl.org (Postfix) with ESMTP id 761B3206\n for <buildroot@buildroot.org>; Wed, 18 Feb 2026 16:32:14 +0000 (UTC)",
            "from localhost (localhost [127.0.0.1])\n by smtp3.osuosl.org (Postfix) with ESMTP id 5BFFE608F5\n for <buildroot@buildroot.org>; Wed, 18 Feb 2026 16:32:14 +0000 (UTC)",
            "from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id U5CmZIp1V4pN for <buildroot@buildroot.org>;\n Wed, 18 Feb 2026 16:32:13 +0000 (UTC)",
            "from mail-wm1-x343.google.com (mail-wm1-x343.google.com\n [IPv6:2a00:1450:4864:20::343])\n by smtp3.osuosl.org (Postfix) with ESMTPS id 81B5E60902\n for <buildroot@buildroot.org>; Wed, 18 Feb 2026 16:32:12 +0000 (UTC)",
            "by mail-wm1-x343.google.com with SMTP id\n 5b1f17b1804b1-4837584120eso161785e9.1\n for <buildroot@buildroot.org>; Wed, 18 Feb 2026 08:32:12 -0800 (PST)",
            "from FRSMI25-GRAVITY.. ([2a01:e0a:943:83d0:6dc6:c524:f7df:4d64])\n by smtp.gmail.com with ESMTPSA id\n 5b1f17b1804b1-483983e8e3dsm20677605e9.20.2026.02.18.08.32.09\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Wed, 18 Feb 2026 08:32:09 -0800 (PST)"
        ],
        "X-Virus-Scanned": [
            "amavis at osuosl.org",
            "amavis at osuosl.org"
        ],
        "X-Comment": "SPF check N/A for local connections - client-ip=140.211.166.142;\n helo=lists1.osuosl.org; envelope-from=buildroot-bounces@buildroot.org;\n receiver=<UNKNOWN> ",
        "DKIM-Filter": [
            "OpenDKIM Filter v2.11.0 smtp3.osuosl.org A690A60A4E",
            "OpenDKIM Filter v2.11.0 smtp3.osuosl.org 81B5E60902"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=buildroot.org;\n\ts=default; t=1771432337;\n\tbh=laYuNYnmefYaT34Ee4j0CxGLh260os3stPemyRaWNGc=;\n\th=To:Cc:Date:In-Reply-To:References:Subject:List-Id:\n\t List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe:\n\t From:Reply-To:From;\n\tb=iAOwI8PPVSELePEJX4KF4rVXvU7ObSf2shl/YyTScUaUBfOekYUFcbd5xF8PeKhuh\n\t YA+VzrDE1E6uZEwzzU6Bt1kv/S74aam7J9iqtfk3SBPkH15JxB2oCsBjb3fGIWqisP\n\t 4PNZdBcH/Z0lXjwoYazlfPmkB97gW1z+r2o1uzi2cCJL5XnKv/X+jR/DAwNW6CX0mM\n\t /NSpKd3q6xBuUD4w5ugvi/VUuwiblyhd9UhMoeARE/EI5cTX/x2SJkFt93Gp3eSoj3\n\t 1NdP+JoX+MgFb10grV25csh1jmMUK34JfzG/rBQRo1Xtm7VgGeCwYlGOqm8lgBsswj\n\t 9Thvb3x6AQPQg==",
        "Received-SPF": "Pass (mailfrom) identity=mailfrom;\n client-ip=2a00:1450:4864:20::343; helo=mail-wm1-x343.google.com;\n envelope-from=fabien.lehoussel@smile.fr; receiver=<UNKNOWN>",
        "DMARC-Filter": "OpenDMARC Filter v1.4.2 smtp3.osuosl.org 81B5E60902",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20230601; t=1771432330; x=1772037130;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n :to:cc:subject:date:message-id:reply-to;\n bh=ZoVLyL39SKoXOPXTChwP5wuAP2uDliUAjyLSoOUBq/w=;\n b=Pop2AEYRjsxe4UxFw3ckEEHF6urW9S0f40uxsW0hbz29P5msjH0wLmvzT+cbv63HyL\n NKDb9WGHlsT2PKAjEwF+qX6BhZZ0igdF83juPt7ufsXFhg7/N21zB8FZl1Uq/K4XOVpR\n tpprDhztXNyqdTansdVame70PITnmJ/dbOIc3yhZUjRamkBCmKFcwJgJzAOCHuXst93w\n 2+4MLMgXMBOvuYe/IjiHmRdAPITQMc6bwAirPmF7AxKDp2233BEdE1zvE1bzviUpWtBN\n BdXm/0/daLxe1TgjwgqYbddIX4VwG9NeTo/3RwPp/wz0BlfCZSn3080uLYdtDFIKBVNX\n rM/g==",
        "X-Gm-Message-State": "AOJu0YxxTDuphffrGEDyDPEqWk+TTUsd9fzVCZ1f0h4afj5dyLPhf0Ac\n 22Ktr3u9L0AKHdtv1I97Vt1neWT7Lxi4uBvEYhBaqJBG8KMMs1GfHolMQgGZeLEWjvCjiXQ4DfS\n DkfYC0CE=",
        "X-Gm-Gg": "AZuq6aL02dw4+zwbspPj8Q+PHBI8vt1m4xiPy7+b6Tx3tA4xorhJd4A/x/gExhROXfw\n a6WrmRfHY+yy+WspeZb7NFUDp04ZTCyC1lOpSMA3S0lT9HzhAX0iqdPWEnG49/i5Icv89ybOVoD\n E4AfvFl5m3XZm2zadYcMxZfBmIvJ4ewVvl2CzS2+iMy1Mr2r/C0F0Oa+vm3YC03J4ypcilfrdsP\n aebNydsiT9JTVGQ1TC7dP+5Ge7CSJjQ6M48vncnnwk2md/93zFDx5w0pIlZQAP+mUNlSdoaPeCy\n ++ZvlngfzEK8TVCKVB2sNSHW1+slYL27/KnVB7xF1O6xkQOsdEumqu222Thmn+nWKUnZev8Aqgi\n tu+gXdQ1VUIrt4rbQeu6tCxGRuEUrScjuzD2/XLOZE+UwIsydd8Vi0ZDgVzx+LINFMhaCVB0kC2\n 6e/Rbz7n+EYRT2R+A3LUbShYwsBRoVXCpFTefrxs2c",
        "X-Received": "by 2002:a05:600c:444f:b0:475:de14:db1e with SMTP id\n 5b1f17b1804b1-48379bd742amr243534805e9.24.1771432329907;\n Wed, 18 Feb 2026 08:32:09 -0800 (PST)",
        "To": "buildroot@buildroot.org",
        "Cc": "Fabien Lehoussel <fabien.lehoussel@smile.fr>",
        "Date": "Wed, 18 Feb 2026 17:32:04 +0100",
        "Message-ID": "<20260218163205.1639035-3-fabien.lehoussel@smile.fr>",
        "X-Mailer": "git-send-email 2.43.0",
        "In-Reply-To": "<20260218163205.1639035-1-fabien.lehoussel@smile.fr>",
        "References": "<20260218163205.1639035-1-fabien.lehoussel@smile.fr>",
        "MIME-Version": "1.0",
        "X-Mailman-Original-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=smile.fr; s=google; t=1771432330; x=1772037130; darn=buildroot.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=ZoVLyL39SKoXOPXTChwP5wuAP2uDliUAjyLSoOUBq/w=;\n b=CbrZDAPUmeZlUoSTF+TTCfmqdUStXb57XcKTktuvuKuu61WgX57yM//n8rywtW4Ktr\n pSmuLKJzsHvqaKixMHtoLw9LOHuEREdB9SpuXqn6FmcQ+oqD+sK+NMgKoFqmQnZ4HMAl\n JdNy/4u0+z4Ju+bEq45FVFRdBbSefpLhrLTKw=",
        "X-Mailman-Original-Authentication-Results": [
            "smtp3.osuosl.org;\n dmarc=pass (p=reject dis=none)\n header.from=smile.fr",
            "smtp3.osuosl.org;\n dkim=pass (1024-bit key) header.d=smile.fr header.i=@smile.fr\n header.a=rsa-sha256 header.s=google header.b=CbrZDAPU"
        ],
        "Subject": "[Buildroot] [PATCH 2/2] support/scripts: add filter-kernel-cve\n script for Linux kernel CVE analysis",
        "X-BeenThere": "buildroot@buildroot.org",
        "X-Mailman-Version": "2.1.30",
        "Precedence": "list",
        "List-Id": "Discussion and development of buildroot <buildroot.buildroot.org>",
        "List-Unsubscribe": "<https://lists.buildroot.org/mailman/options/buildroot>,\n <mailto:buildroot-request@buildroot.org?subject=unsubscribe>",
        "List-Archive": "<http://lists.buildroot.org/pipermail/buildroot/>",
        "List-Post": "<mailto:buildroot@buildroot.org>",
        "List-Help": "<mailto:buildroot-request@buildroot.org?subject=help>",
        "List-Subscribe": "<https://lists.buildroot.org/mailman/listinfo/buildroot>,\n <mailto:buildroot-request@buildroot.org?subject=subscribe>",
        "From": "Fabien Lehoussel via buildroot <buildroot@buildroot.org>",
        "Reply-To": "Fabien Lehoussel <fabien.lehoussel@smile.fr>",
        "Content-Type": "text/plain; charset=\"utf-8\"",
        "Content-Transfer-Encoding": "base64",
        "Errors-To": "buildroot-bounces@buildroot.org",
        "Sender": "\"buildroot\" <buildroot-bounces@buildroot.org>"
    },
    "content": "Add a new script to filter and analyze Linux kernel CVEs from Buildroot's\nCycloneDX SBOM report. The script removes CVEs that are definitively NOT\napplicable to the build by cross-referencing affected file information\nfrom the official CVE database (CNA - CVE Numbering Authority) against\nthe kernel's compiled files (from compile_commands.json).\n\nThis enables removing false positives while keeping potentially applicable\nCVEs that require further investigation.\n\nFeatures:\n- Loads CycloneDX SBOM reports from file or stdin\n- Extracts CVEs affecting the \"linux\" component\n- Queries the official CVE Numbering Authority (CNA) database (cvelistV5)\n  for affected file information (programFiles)\n- Matches CVE program files against actually compiled kernel files\n- Categorizes CVEs into three groups:\n  * Confirmed applicable: CVEs where affected files are compiled (kept)\n  * Not applicable: CVEs where NO affected files are compiled (removed)\n  * Insufficient data: CVEs not found in CNA database (kept for review)\n- Outputs filtered SBOM report removing only definitively non-applicable CVEs\n- Generates detailed analysis report with file-to-CVE mapping\n- Optionally auto-clones or updates CNA database from GitHub\n- Supports stdin/stdout for pipeline integration with cve-check\n\nUsage examples:\n  # Using in a pipeline\n  make show-info | utils/generate-cyclonedx | support/scripts/cve-check | \\\n      support/scripts/filter-kernel-cve \\\n        --cc-path output/build/linux-X.Y.Z/compile_commands.json \\\n        --cna-path cvelistV5 > sbom_filtered.json\n\n  # Using with files\n  support/scripts/filter-kernel-cve \\\n    --buildroot-report sbom.json \\\n    --cc-path compile_commands.json \\\n    --cna-path cvelistV5 \\\n    --output-report sbom_filtered.json \\\n    --output-details cve_details.json \\\n    --log-file filter.log --log-level DEBUG \\\n    --auto-update\n\nSigned-off-by: Fabien Lehoussel <fabien.lehoussel@smile.fr>\n---\n support/scripts/filter-kernel-cve | 592 ++++++++++++++++++++++++++++++\n 1 file changed, 592 insertions(+)\n create mode 100755 support/scripts/filter-kernel-cve",
    "diff": "diff --git a/support/scripts/filter-kernel-cve b/support/scripts/filter-kernel-cve\nnew file mode 100755\nindex 0000000000..a3181aae0f\n--- /dev/null\n+++ b/support/scripts/filter-kernel-cve\n@@ -0,0 +1,592 @@\n+#!/usr/bin/env python3\n+\"\"\"\n+Analyze Linux kernel CVEs from Buildroot CycloneDX SBOM report.\n+\n+This script takes as input:\n+  1. The CycloneDX SBOM report generated by Buildroot (containing CVEs)\n+  2. The compile_commands.json file from the kernel build\n+  3. The directory containing CVEs in CNA 5.x format (https://github.com/CVEProject/cvelistV5)\n+\n+For each CVE affecting the \"linux\" component:\n+  - Load the CVE from the CNA database\n+  - Extract affected files (programFiles)\n+  - Check if these files are in compile_commands.json\n+  - If YES → CVE is applicable (keep in report)\n+  - If NO → CVE is not applicable (remove from report)\n+\n+Outputs:\n+  1. Updated Buildroot SBOM report (only non-applicable CVEs removed)\n+  2. Detailed CVE analysis report with breakdown by category (optional)\n+\n+Categories in the analysis:\n+  - Applicable CVEs: CVEs affecting compiled files\n+  - Removed CVEs: CVEs not affecting any compiled files\n+  - Insufficient data CVEs: CVEs kept due to lack of information\n+\n+Example usage:\n+make show-info | \\\n+    utils/generate-cyclonedx | \\\n+    support/script/cve-check --nvd-path dl/buildroot-nvd/ | \\\n+    filter-kernel-cve \\\n+        --cc-path  /path/to/output/build/linux-X.Y.Z/compile_commands.json \\\n+        --cna-path /path/to/cvelistV5 \\\n+        --no-cna-update \\\n+        --output-details /path/to/cve_details.json \\\n+        --log-file /path/to/filter.log \\\n+        --log-level=DEBUG\n+or \n+    filter-kernel-cve \\\n+        --in-file /path/to/sbom.json \\\n+        --cc-path  /path/to/compile_commands.json \\\n+        --cna-path /path/to/cvelistV5 \\\n+        --no-cna-update \\\n+        --output-details /path/to/cve_details.json \\\n+        --log-file /path/to/filter.log \\\n+        --log-level=DEBUG\n+\"\"\"\n+\n+import argparse\n+import json\n+import logging\n+import sys\n+import subprocess\n+import os\n+from pathlib import Path\n+from typing import Optional\n+\n+# CVE repository URL\n+CVE_REPO_URL = \"https://github.com/CVEProject/cvelistV5.git\"\n+\n+# Configure logging\n+logger = logging.getLogger(__name__)\n+\n+def parse_args():\n+    parser = argparse.ArgumentParser(\n+        description=\"Filter Linux kernel CVEs from Buildroot CycloneDX SBOM report\"\n+    )\n+\n+    parser.add_argument(\n+        \"-i\", \"--in-file\",\n+        type=str,\n+        default=None,\n+        help=\"CycloneDX SBOM JSON report file (default: read from stdin)\",\n+    )\n+    parser.add_argument(\n+        \"-o\",\"--out-file\",\n+        type=str,\n+        default=None,\n+        help=\"Updated Buildroot SBOM report (default: write to stdout)\",\n+    )\n+    parser.add_argument(\n+        \"--cc-path\",\n+        required=True,\n+        help=\"Kernel compile_commands.json file\",\n+    )\n+    parser.add_argument(\n+        \"--cna-path\",\n+        required=True,\n+        help=\"Directory containing CVEs in CNA 5.x format (JSON files)\",\n+    )\n+    parser.add_argument(\n+        \"--no-cna-update\",\n+        action=\"store_true\",\n+        help=\"Doesn't clone or update CVE database\",\n+    )\n+    parser.add_argument(\n+        \"--output-details\",\n+        default=\"kernel_cve_details.json\",\n+        help=\"JSON file with detailed CVE analysis\",\n+    )\n+    parser.add_argument(\n+        \"--log-file\",\n+        default=None,\n+        help=\"Log file path (if not set, no logs are displayed)\",\n+    )\n+    parser.add_argument(\n+        \"--log-level\",\n+        choices=[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\"],\n+        default=\"INFO\",\n+        help=\"Set logging level (default: INFO)\",\n+    )\n+    return parser.parse_args()\n+\n+\n+def setup_logging(log_level: str, log_file: Optional[str]):\n+    \"\"\"Configure logging only if log_file is specified.\"\"\"\n+    if not log_file:\n+        # Disable logging if no log file specified\n+        logging.disable(logging.CRITICAL)\n+        return\n+\n+    # Determine log level\n+    level = getattr(logging, log_level)\n+\n+    # Create formatter\n+    formatter = logging.Formatter('%(message)s')\n+\n+    # Configure root logger\n+    root_logger = logging.getLogger()\n+    root_logger.setLevel(level)\n+\n+    # Remove existing handlers\n+    for handler in root_logger.handlers[:]:\n+        root_logger.removeHandler(handler)\n+\n+    # Add file handler\n+    file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')\n+    file_handler.setLevel(level)\n+    file_handler.setFormatter(formatter)\n+    root_logger.addHandler(file_handler)\n+\n+\n+# ---------------------------------------------------------------------------\n+# CVE Database management\n+# ---------------------------------------------------------------------------\n+def sync_cve_database(cna_dir: str) -> None:\n+    \"\"\"\n+    Clone or update the CVE database from GitHub.\n+    \n+    If the directory doesn't exist, clone the repository.\n+    If it exists and is a git repository, pull the latest changes.\n+    \"\"\"\n+    cna_path = Path(cna_dir)\n+    git_dir = cna_path / \".git\"\n+    \n+    try:\n+        if git_dir.exists():\n+            # Directory is a git repo, pull latest changes\n+            logger.info(f\"Updating CVE database from {CVE_REPO_URL}...\")\n+            subprocess.run(\n+                [\"git\", \"pull\"],\n+                cwd=cna_dir,\n+                stdout=subprocess.DEVNULL,\n+                stderr=subprocess.DEVNULL,\n+                check=True,\n+            )\n+            logger.info(\"CVE database updated successfully\")\n+        else:\n+            # Clone the repository\n+            logger.info(f\"Cloning CVE database from {CVE_REPO_URL}...\")\n+            cna_path.parent.mkdir(parents=True, exist_ok=True)\n+            subprocess.run(\n+                [\"git\", \"clone\", CVE_REPO_URL, cna_dir],\n+                stdout=subprocess.DEVNULL,\n+                stderr=subprocess.DEVNULL,\n+                check=True,\n+            )\n+            logger.info(\"CVE database cloned successfully\")\n+    except subprocess.CalledProcessError as e:\n+        logger.error(f\"Failed to sync CVE database: {e}\")\n+        sys.exit(1)\n+    except FileNotFoundError:\n+        logger.error(\"git is not installed or not in PATH\")\n+        sys.exit(1)\n+\n+\n+# ---------------------------------------------------------------------------\n+# Load Buildroot CycloneDX SBOM report\n+# ---------------------------------------------------------------------------\n+def load_cve_check_report(input_file: Optional[str]) -> dict:\n+    \"\"\"\n+    Load the CycloneDX SBOM JSON report from file or stdin.\n+    \n+    Expected format:\n+    {\n+      \"bomFormat\": \"CycloneDX\",\n+      \"components\": [...],\n+      \"vulnerabilities\": [...],\n+      \"dependencies\": [...]\n+    }\n+    \"\"\"\n+    if input_file:\n+        p = Path(input_file)\n+        if not p.exists():\n+            logger.error(f\"Input report not found: {input_file}\")\n+            sys.exit(1)\n+        with open(p, encoding=\"utf-8\") as f:\n+            data = json.load(f)\n+    else:\n+        # Read from stdin\n+        try:\n+            data = json.load(sys.stdin)\n+        except json.JSONDecodeError as e:\n+            logger.error(f\"Failed to parse JSON from stdin: {e}\")\n+            sys.exit(1)\n+\n+    if data.get(\"bomFormat\") != \"CycloneDX\":\n+        logger.error(f\"Not a CycloneDX SBOM format. Found: {data.get('bomFormat')}\")\n+        sys.exit(1)\n+\n+    logger.info(\"CycloneDX SBOM report loaded\")\n+    return data\n+\n+\n+def extract_linux_cves(cve_check_report: dict) -> list[dict]:\n+    \"\"\"\n+    Extract CVEs affecting the \"linux\" component from Buildroot CycloneDX SBOM.\n+\n+    Returns a list of dicts with CVE info.\n+    \"\"\"\n+    linux_cves = []\n+\n+    # Extract Linux version from components[]\n+    linux_version = None\n+    for component in cve_check_report.get(\"components\", []):\n+        if component.get(\"name\") == \"linux\":\n+            linux_version = component.get(\"version\", \"unknown\")\n+            logger.debug(f\"Found Linux version in SBOM: {linux_version}\")\n+            break\n+\n+    # Extract CVEs from vulnerabilities[]\n+    total_vulns = len(cve_check_report.get(\"vulnerabilities\", []))\n+    logger.debug(f\"Total vulnerabilities in SBOM: {total_vulns}\")\n+\n+    for vuln in cve_check_report.get(\"vulnerabilities\", []):\n+        cve_id = vuln.get(\"id\", \"\")\n+        if not cve_id.startswith(\"CVE-\"):\n+            continue\n+\n+        # Check if this CVE affects \"linux\"\n+        affects = vuln.get(\"affects\", [])\n+        if not any(a.get(\"ref\") == \"linux\" for a in affects):\n+            continue\n+\n+        logger.debug(f\"Found Linux CVE: {cve_id}\")\n+\n+        linux_cves.append({\n+            \"cve_id\":         cve_id,\n+            \"original_entry\": vuln,\n+        })\n+\n+    logger.info(f\"{len(linux_cves)} CVE(s) affecting 'linux' component found\")\n+    return linux_cves\n+\n+\n+# ---------------------------------------------------------------------------\n+# Load compile_commands.json\n+# ---------------------------------------------------------------------------\n+def load_compiled_files(compile_commands_path: str) -> set[str]:\n+    \"\"\"\n+    Load compile_commands.json and extract the list of compiled files.\n+    Returns a set of relative paths (e.g., 'drivers/net/foo.c').\n+    \"\"\"\n+    p = Path(compile_commands_path)\n+    if not p.exists():\n+        logger.error(f\"compile_commands.json not found: {compile_commands_path}\")\n+        sys.exit(1)\n+    \n+    with open(p, encoding=\"utf-8\") as f:\n+        commands = json.load(f)\n+\n+    logger.debug(f\"Loaded compile_commands.json with {len(commands)} entries\")\n+\n+    if not isinstance(commands, list):\n+        logger.error(\"Unexpected format in compile_commands.json\")\n+        sys.exit(1)\n+\n+    # Infer kernel_dir from compile_commands\n+    kernel_dir = None\n+    if commands:\n+        first_dir = Path(commands[0].get(\"directory\", \"\"))\n+        kernel_dir = str(first_dir.resolve())\n+        logger.debug(f\"Inferred kernel_dir from compile_commands: {kernel_dir}\")\n+\n+    kernel_root = Path(kernel_dir).resolve() if kernel_dir else None\n+    logger.debug(f\"Using kernel root: {kernel_root}\")\n+    compiled_files = set()\n+    \n+    for entry in commands:\n+        file_path = Path(entry.get(\"file\", \"\"))\n+        \n+        # Make relative to kernel_root if possible\n+        if kernel_root:\n+            try:\n+                rel_path = file_path.resolve().relative_to(kernel_root)\n+                compiled_files.add(str(rel_path))\n+            except ValueError:\n+                # File outside kernel tree → ignore\n+                continue\n+        else:\n+            # Fallback: use path as-is\n+            compiled_files.add(str(file_path))\n+\n+    logger.info(f\"{len(compiled_files)} compiled file(s) extracted from compile_commands.json\")\n+    return compiled_files\n+\n+# ---------------------------------------------------------------------------\n+# Load CVE from CNA database\n+# ---------------------------------------------------------------------------\n+def load_cve_from_cna(cna_dir: str, cve_id: str) -> Optional[dict]:\n+    \"\"\"\n+    Load a specific CVE from the CNA directory.\n+\n+    Searches for a file named CVE-YYYY-NNNNN.json or similar.\n+    Returns a dict with extracted info, or None if not found/invalid.\n+    \"\"\"\n+    cna_path = Path(cna_dir)\n+\n+    # Construct possible file names\n+    possible_names = [\n+        f\"{cve_id}.json\",\n+        f\"{cve_id.replace('CVE-', '')}.json\",\n+    ]\n+\n+    cve_file = None\n+    for name in possible_names:\n+        candidate = cna_path / name\n+        if candidate.exists():\n+            cve_file = candidate\n+            logger.debug(f\"Found CVE file: {candidate}\")\n+            break\n+\n+    # Recursive search as last resort\n+    if not cve_file:\n+        logger.debug(f\"Searching recursively for {cve_id}.json in {cna_dir}\")\n+        matches = list(cna_path.rglob(f\"{cve_id}.json\"))\n+        if matches:\n+            cve_file = matches[0]\n+            logger.debug(f\"Found CVE file: {cve_file}\")\n+\n+    if not cve_file:\n+        logger.debug(f\"CVE file not found for {cve_id}\")\n+        return None\n+\n+    try:\n+        with open(cve_file, encoding=\"utf-8\") as f:\n+            data = json.load(f)\n+    except (json.JSONDecodeError, OSError):\n+        return None\n+\n+    # Verify CNA 5.x format\n+    if data.get(\"dataType\") != \"CVE_RECORD\":\n+        return None\n+\n+    cna = data.get(\"containers\", {}).get(\"cna\", {})\n+    if not cna:\n+        return None\n+\n+    # Extract programFiles\n+    program_files = []\n+    for affected in cna.get(\"affected\", []):\n+        if affected.get(\"vendor\") == \"Linux\" and affected.get(\"product\") == \"Linux\":\n+            program_files.extend(affected.get(\"programFiles\", []))\n+\n+    # Deduplicate\n+    program_files = sorted(set(program_files))\n+\n+    logger.debug(f\"Extracted {len(program_files)} program file(s) from CVE\")\n+    if program_files and logger.isEnabledFor(logging.DEBUG):\n+        for pf in program_files:\n+            logger.debug(f\"  - {pf}\")\n+\n+    return {\n+        \"cve_id\":        cve_id,\n+        \"program_files\": program_files,\n+    }\n+\n+\n+# ---------------------------------------------------------------------------\n+# File matching\n+# ---------------------------------------------------------------------------\n+def match_files(program_files: list[str], compiled_files: set[str]) -> list[str]:\n+    \"\"\"\n+    Return compiled files that match the CVE's programFiles.\n+    Uses suffix matching to handle path differences.\n+    \"\"\"\n+    matched = []\n+\n+    logger.debug(f\"Checking {len(program_files)} program file(s) with compiled files\")\n+\n+    for prog_file in program_files:\n+        # Normalize: remove leading slash\n+        prog_norm = prog_file.lstrip(\"/\")\n+        logger.debug(f\"Checking program file: {prog_norm}\")\n+\n+        found = False\n+        for compiled in compiled_files:\n+            # Exact match\n+            if compiled == prog_norm:\n+                if compiled not in matched:\n+                    matched.append(compiled)\n+                    logger.debug(f\"  ✓ Exact match with: {compiled}\")\n+                    found = True\n+                continue\n+\n+            # Suffix match (CVE may have different path)\n+            if compiled.endswith(\"/\" + prog_norm) or prog_norm.endswith(\"/\" + compiled):\n+                if compiled not in matched:\n+                    matched.append(compiled)\n+                    logger.debug(f\"  ✓ Suffix match with: {compiled}\")\n+                    found = True\n+\n+        if not found:\n+            logger.debug(f\"  ✗ No match found\")\n+\n+    return sorted(matched)\n+\n+\n+# ---------------------------------------------------------------------------\n+# Helper: Save JSON file with directory creation\n+# ---------------------------------------------------------------------------\n+def save_json_report(filepath: str, data: dict, description: str) -> None:\n+    \"\"\"Saves a JSON report, creating directories if needed.\"\"\"\n+    output_path = Path(filepath)\n+    output_path.parent.mkdir(parents=True, exist_ok=True)\n+    \n+    with open(output_path, \"w\", encoding=\"utf-8\") as f:\n+        json.dump(data, f, indent=2, ensure_ascii=False)\n+    logger.info(f\"[OK] {description} → {filepath}\")\n+\n+\n+def write_output_file(output_file: Optional[str], data: dict) -> None:\n+    \"\"\"Write JSON output to file or stdout.\"\"\"\n+    if output_file:\n+        output_path = Path(output_file)\n+        output_path.parent.mkdir(parents=True, exist_ok=True)\n+        with open(output_path, \"w\", encoding=\"utf-8\") as f:\n+            json.dump(data, f, indent=2, ensure_ascii=False)\n+        logger.info(f\"[OK] Filtered SBOM report → {output_file}\")\n+    else:\n+        # Write to stdout\n+        json.dump(data, sys.stdout, indent=2, ensure_ascii=False)\n+        sys.stdout.write('\\n')\n+\n+\n+# ---------------------------------------------------------------------------\n+# Main\n+# ---------------------------------------------------------------------------\n+def main():\n+    args = parse_args()\n+\n+    # Setup logging BEFORE any logging calls\n+    setup_logging(args.log_level, args.log_file)\n+\n+    # Sync CVE database if requested\n+    if not args.no_cna_update:\n+        logger.info(\"[STEP 0] Syncing CVE database...\")\n+        sync_cve_database(args.cna_path)\n+\n+    # Load data\n+    logger.info(\"[STEP 1] Loading Buildroot SBOM report...\")\n+    cve_check_report = load_cve_check_report(args.in_file)\n+    linux_cves = extract_linux_cves(cve_check_report)\n+\n+    if not linux_cves:\n+        logger.info(\"No CVEs affecting 'linux' component in the report.\")\n+        logger.info(\"Nothing to filter, report unchanged.\")\n+        write_output_file(args.out_file, cve_check_report)\n+        return\n+\n+    logger.info(\"[STEP 2] Loading compile_commands.json...\")\n+    compiled_files = load_compiled_files(args.cc_path)\n+    \n+    logger.info(f\"[STEP 3] Analyzing {len(linux_cves)} CVE(s)...\")\n+\n+    cves_kept = []      # Confirmed applicable CVEs\n+    cves_removed = []   # Non-applicable CVEs (files not compiled)\n+    cves_insufficient_data = []   # CVEs not found in CNA database or do not contain sufficient information\n+\n+    cve_details = {}    # CVE_ID → affected compiled files\n+\n+    for i, linux_cve in enumerate(linux_cves, 1):\n+        cve_id = linux_cve[\"cve_id\"]\n+        logger.info(f\"[{i}/{len(linux_cves)}] {cve_id}...\")\n+        \n+        # Load CVE from CNA database\n+        cve_cna = load_cve_from_cna(args.cna_path, cve_id)\n+        \n+        if not cve_cna:\n+            logger.info(f\"  ✗ Not found in CNA database\")\n+            cves_insufficient_data.append(cve_id)\n+            continue\n+\n+        # Check if any compiled files are affected\n+        if cve_cna[\"program_files\"]:\n+            matched = match_files(cve_cna[\"program_files\"], compiled_files)\n+\n+            # Always keep program_files details\n+            cve_details[cve_id] = {\n+                \"program_files\": cve_cna[\"program_files\"],\n+                \"matched_compiled\": matched,\n+            }\n+\n+            if matched:\n+                logger.info(f\"  ✓ Applicable ({len(matched)} compiled file(s))\")\n+                cves_kept.append(cve_id)\n+            else:\n+                logger.info(f\"  ✗ Not applicable (files not compiled)\")\n+                cves_removed.append(cve_id)\n+        else:\n+            logger.info(f\"  ✗ No program files info, keeping CVE\")\n+            cves_insufficient_data.append(cve_id)\n+\n+    # ---------------------------------------------------------------------------\n+    # Reconstruct updated Buildroot SBOM report (preserve all fields)\n+    # ---------------------------------------------------------------------------\n+    logger.info(\"[STEP 4] Generating filtered report...\")\n+    \n+    # Filter out only non-applicable CVEs (keep applicable + insufficient_data)\n+    if \"vulnerabilities\" in cve_check_report:\n+        cve_check_report[\"vulnerabilities\"] = [\n+            vuln for vuln in cve_check_report[\"vulnerabilities\"]\n+            if vuln.get(\"id\") not in cves_removed\n+        ]\n+\n+    write_output_file(args.out_file, cve_check_report)\n+\n+    # ---------------------------------------------------------------------------\n+    # Generate CVE details file\n+    # ---------------------------------------------------------------------------\n+    if args.output_details:\n+        details_output = {\n+            \"summary\": {\n+                \"total_cves_analyzed\":      len(linux_cves),\n+                \"cves_applicable\":          len(cves_kept),\n+                \"cves_insufficient_data\":   len(cves_insufficient_data),\n+                \"cves_removed\":             len(cves_removed)\n+            },\n+            \"applicable_cves\": {cve_id: cve_details[cve_id] for cve_id in cves_kept},\n+            \"removed_cves\": {cve_id: cve_details[cve_id] for cve_id in cves_removed},\n+            \"cves_insufficient_data\": cves_insufficient_data,\n+        }\n+        save_json_report(args.output_details, details_output, \"CVE details\")\n+\n+    # ---------------------------------------------------------------------------\n+    # Display summary\n+    # ---------------------------------------------------------------------------\n+    logger.info(f\"{'='*60}\")\n+    logger.info(f\"  Confirmed applicable CVEs     : {len(cves_kept)}\")\n+    logger.info(f\"  Insufficient data (kept)      : {len(cves_insufficient_data)}\")\n+    logger.info(f\"  Non-applicable CVEs (removed) : {len(cves_removed)}\")\n+\n+    logger.info(f\"{'='*60}\")\n+\n+    # Show details in DEBUG mode\n+    if logger.isEnabledFor(logging.DEBUG):\n+        if cves_kept:\n+            logger.debug(f\"\\n{'─'*60}\")\n+            logger.debug(f\"CONFIRMED APPLICABLE CVEs\")\n+            logger.debug(f\"{'─'*60}\")\n+            for cve_id in sorted(cves_kept):\n+                details = cve_details[cve_id]\n+                logger.debug(f\"{cve_id}\")\n+                logger.debug(f\"  Affected files: {len(details['matched_compiled'])}\")\n+                for f in details[\"matched_compiled\"]:\n+                    logger.debug(f\"    - {f}\")\n+\n+        if cves_insufficient_data:\n+            logger.debug(f\"\\n{'─'*60}\")\n+            logger.debug(f\"UNKNOWN STATE CVEs\")\n+            logger.debug(f\"{'─'*60}\")\n+            for cve_id in sorted(cves_insufficient_data):\n+                logger.debug(f\"{cve_id}\")\n+        if cves_removed:\n+            logger.debug(f\"\\n{'─'*60}\")\n+            logger.debug(f\"REMOVED CVEs\")\n+            logger.debug(f\"{'─'*60}\")\n+            for cve_id in sorted(cves_removed):\n+                logger.debug(f\"{cve_id}\")\n+\n+\n+if __name__ == \"__main__\":\n+    main()\n\\ No newline at end of file\n",
    "prefixes": [
        "2/2"
    ]
}