* [Buildroot] [PATCH v3 0/2] Linux kernel CVE filtering
@ 2026-02-25 10:55 Fabien Lehoussel via buildroot
2026-02-25 10:55 ` [Buildroot] [PATCH 1/2] linux/linux.mk: add generation of compile_commands.json Fabien Lehoussel via buildroot
2026-02-25 10:56 ` [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files Fabien Lehoussel via buildroot
0 siblings, 2 replies; 4+ messages in thread
From: Fabien Lehoussel via buildroot @ 2026-02-25 10:55 UTC (permalink / raw)
To: buildroot; +Cc: Thomas Perale, Fabien Lehoussel
Changes from v2:
- Mark non-compiled kernel CVEs as "not_affected" with "code_not_present"
justification instead of removing them, preserving audit trail
- Fix help text formatting with RawDescriptionHelpFormatter for better readability
Changes from v1:
Following Thomas' suggestion (https://lists.buildroot.org/pipermail/buildroot/2026-February/797289.html),
this v2 implements kernel CVE filtering directly into cve-check rather than
as a separate script.
1. Kernel build generates compile_commands.json (optional, via Kconfig)
2. cve-check script enhanced with optional kernel CVE filtering
- Automatically activates when --cc-path and --cna-path are provided
- Uses CNA database to determine which files are affected by each CVE
- Matches against compile_commands.json to filter false positives
- Marks CVEs where NO affected files are compiled as "not_affected"
with "code_not_present" justification
- Keeps CVEs where affected files ARE compiled
- Keeps uncertain CVEs (CNA database lacks information) for review
Fabien Lehoussel (2):
linux/linux.mk: add generation of compile_commands.json
support/scripts/cve-check: add kernel CVE filtering based on compiled
files
linux/Config.in | 20 +++
linux/linux.mk | 12 ++
support/scripts/cve-check | 119 ++++++++++++++-
support/scripts/cve.py | 294 +++++++++++++++++++++++++++++++++++---
4 files changed, 418 insertions(+), 27 deletions(-)
--
2.43.0
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
^ permalink raw reply [flat|nested] 4+ messages in thread
* [Buildroot] [PATCH 1/2] linux/linux.mk: add generation of compile_commands.json
2026-02-25 10:55 [Buildroot] [PATCH v3 0/2] Linux kernel CVE filtering Fabien Lehoussel via buildroot
@ 2026-02-25 10:55 ` Fabien Lehoussel via buildroot
2026-02-25 10:56 ` [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files Fabien Lehoussel via buildroot
1 sibling, 0 replies; 4+ messages in thread
From: Fabien Lehoussel via buildroot @ 2026-02-25 10:55 UTC (permalink / raw)
To: buildroot; +Cc: Thomas Perale, Fabien Lehoussel
Enable automatic generation of compile_commands.json during the kernel
build process to facilitate security analysis and CVE filtering.
The compile_commands.json file is generated after the kernel build
using the native kernel target (available since Linux 5.4). It
contains the complete list of compiled source files with their
compilation commands and flags.
The generated file is saved as linux_compile_commands.json in the
binaries directory and can be used to:
- Extract the exact list of compiled kernel files
- Cross-reference with CVE databases (NVD, CNA) to filter relevant
vulnerabilities
In the future, this information could be leveraged to:
- Map affected files to their corresponding Kconfig CONFIG_ options
- Determine which CVEs are actually applicable based on the current
build configuration
- Provide more accurate vulnerability impact analysis
Signed-off-by: Fabien Lehoussel <fabien.lehoussel@smile.fr>
---
linux/Config.in | 20 ++++++++++++++++++++
linux/linux.mk | 12 ++++++++++++
2 files changed, 32 insertions(+)
diff --git a/linux/Config.in b/linux/Config.in
index 73a8059523..48faf1a7d8 100644
--- a/linux/Config.in
+++ b/linux/Config.in
@@ -519,6 +519,26 @@ config BR2_LINUX_KERNEL_INSTALL_TARGET
/boot if DTBs have been generated by the kernel build
process.
+
+config BR2_LINUX_KERNEL_COMPILE_COMMANDS
+ bool "Generate compile_commands.json file"
+ help
+ Generate compile_commands.json during the Linux kernel build.
+ This file contains the exact list of all compiled kernel files
+ with the current build options/configuration in JSON format
+
+ The generated file is copied to the binaries directory as
+ linux_compile_commands.json and can be used for:
+ - Static analysis and code indexing tools
+ - Cross-referencing with CVE databases (NVD, CNA) to determine
+ which vulnerabilities are actually applicable to the current
+ kernel build configuration
+
+ Note: This option requires Linux kernel 5.4 or newer.
+
+ If unsure, say N.
+
+
config BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL
bool "Needs host OpenSSL"
help
diff --git a/linux/linux.mk b/linux/linux.mk
index c61089bfe0..649921a87b 100644
--- a/linux/linux.mk
+++ b/linux/linux.mk
@@ -550,6 +550,18 @@ define LINUX_BUILD_CMDS
$(LINUX_APPEND_DTB)
endef
+define LINUX_GENERATE_COMPILE_COMMANDS
+ @$(call MESSAGE,"Generating compile_commands.json")
+ $(Q)$(LINUX_MAKE_ENV) $(BR2_MAKE) $(LINUX_MAKE_FLAGS) \
+ -C $(@D) \
+ compile_commands.json
+ cp $(@D)/compile_commands.json $(BINARIES_DIR)/linux_compile_commands.json
+endef
+
+ifeq ($(BR2_LINUX_KERNEL_GENERATE_COMPILE_COMMANDS),y)
+LINUX_POST_BUILD_HOOKS += LINUX_GENERATE_COMPILE_COMMANDS
+endif
+
ifeq ($(BR2_LINUX_KERNEL_APPENDED_DTB),y)
# When a DTB was appended, install the potential several images with
# appended DTBs.
--
2.43.0
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files
2026-02-25 10:55 [Buildroot] [PATCH v3 0/2] Linux kernel CVE filtering Fabien Lehoussel via buildroot
2026-02-25 10:55 ` [Buildroot] [PATCH 1/2] linux/linux.mk: add generation of compile_commands.json Fabien Lehoussel via buildroot
@ 2026-02-25 10:56 ` Fabien Lehoussel via buildroot
2026-02-27 9:50 ` Thomas Perale via buildroot
1 sibling, 1 reply; 4+ messages in thread
From: Fabien Lehoussel via buildroot @ 2026-02-25 10:56 UTC (permalink / raw)
To: buildroot; +Cc: Thomas Perale, Fabien Lehoussel
Introduce optional filtering for Linux kernel CVEs using compile_commands.json
and the CNA database [1].
This reduces false positives by marking CVEs that don't
affect compiled files as "not_affected" with "code_not_present" justification,
while reporting only CVEs that affect compiled files, enhancing the relevance
of CVE reports for specific builds.
Changes:
- Add CVEDatabase class for generic CVE database management (clone/pull)
supporting both NVD and CNA databases with automatic sync capability
Database path argument is the repository root directly (no automatic 'git' subfolder)
enabling seamless CI/CD integration and clearer path semantics
- Add CVE_LINUX class for specialized Linux kernel CVE analysis
- Load compiled files from compile_commands.json
- Match CVE affected files from CNA database against compiled files
- Categorize CVEs as applicable, not applicable, or insufficient data
- Enhance cve-check script with optional kernel CVE filtering
- Activate filtering automatically when --cc-path and --cna-path are provided
- Replace --no-nvd-update with generic --no-db-update flag for both databases
- Mark non-compiled kernel CVEs as "not_affected" with "code_not_present" justification
- Preserve audit trail by keeping all vulnerabilities in SBOM
- Keeps potentially applicable and uncertain Linux CVEs for manual review
Usage:
make show-info | utils/generate-cyclonedx | support/script/cve-check \
--nvd-path dl/buildroot-nvd/ \
--cc-path output/images/linux_compile_commands.json \
--cna-path dl/buildroot-cvelistV5 \
--no-db-update
Requires:
- linux_compile_commands.json from kernel build (BR2_LINUX_KERNEL_COMPILE_COMMANDS)
[1] CNA database: https://github.com/CVEProject/cvelistV5.git
Signed-off-by: Fabien Lehoussel <fabien.lehoussel@smile.fr>
---
support/scripts/cve-check | 119 ++++++++++++++-
support/scripts/cve.py | 294 +++++++++++++++++++++++++++++++++++---
2 files changed, 386 insertions(+), 27 deletions(-)
diff --git a/support/scripts/cve-check b/support/scripts/cve-check
index ff14e4b238..1e3743248a 100755
--- a/support/scripts/cve-check
+++ b/support/scripts/cve-check
@@ -7,11 +7,20 @@
# The NVD database is cloned using a mirror of it and the content is compared
# locally.
#
+# For Linux kernel CVEs, optionally filters them based on compiled files using
+# the CNA database (cvelistV5) and compile_commands.json matching.
+#
# Example usage:
# $ make show-info | utils/generate-cyclonedx | support/script/cve-check --nvd-path dl/buildroot-nvd/
+#
+# With kernel CVE filtering:
+# $ make show-info | utils/generate-cyclonedx | support/script/cve-check \
+# --nvd-path dl/buildroot-nvd/ \
+# --cc-path output/images/linux_compile_commands.json \
+# --cna-path dl/buildroot-cvelistV5
from collections import defaultdict
from pathlib import Path
-from typing import TypedDict
+from typing import TypedDict, Optional
import argparse
import sys
import json
@@ -29,6 +38,16 @@ database.
The NVD database is cloned using a mirror of it and the content is compared
locally.
+
+For Linux kernel CVEs, optionally filters them based on compiled files using
+the CNA database (cvelistV5) and compile_commands.json matching.
+
+When kernel CVE filtering is enabled (via --cc-path and --cna-path):
+- Marks false positives as "not_affected": CVEs where NO affected files are compiled
+ with "code_not_present" justification, preserving complete audit trail
+- Keeps potentially applicable CVEs: where affected files ARE compiled
+- Keeps uncertain CVEs: where CNA database lacks sufficient information
+
"""
@@ -281,8 +300,70 @@ def enrich_vulnerabilities(nvd_path: Path, sbom):
vuln_append_or_update_affects_if_exists(vulnerabilities, vulnerability)
+def analyze_kernel_cves(sbom, cve_linux: cvecheck.CVE_LINUX, compiled_files: set):
+ """
+ Analyze Linux kernel CVEs based on compiled files.
+
+ Marks CVEs where NO affected files are compiled as "not_affected" with
+ "code_not_present" justification, preserving complete audit trail.
+ Keeps CVEs where affected files ARE compiled or insufficient data.
+
+ Args:
+ sbom (dict): Input SBOM with vulnerabilities.
+ cve_linux (CVE_LINUX): CVE_LINUX instance for analysis.
+ compiled_files (set): Set of compiled kernel files.
+ """
+ vulnerabilities = sbom.get("vulnerabilities", [])
+
+ # Process vulnerabilities: mark non-applicable kernel CVEs as not_affected
+ for vuln in vulnerabilities:
+ cve_id = vuln.get("id", "")
+
+ # Check if this CVE affects "linux" component
+ affects = vuln.get("affects", [])
+ affects_linux = any(a.get("ref") == "linux" for a in affects)
+
+ if not affects_linux or not cve_id.startswith("CVE-"):
+ # Not a kernel CVE, leave it unchanged
+ continue
+
+ # Check if kernel CVE is applicable
+ status = cve_linux.affects(cve_id, compiled_files)
+
+ if status == cvecheck.CVE_LINUX.CVE_NOT_APPLICABLE:
+ # Mark as not_affected since code is not present
+ vuln["analysis"] = {
+ "state": "not_affected",
+ "justification": "code_not_present"
+ }
+
+ sbom["vulnerabilities"] = vulnerabilities
+
+def apply_kernel_cve_filtering(sbom, cve_linux, cc_path):
+ """
+ Apply kernel CVE filtering based on compiled files.
+
+ Args:
+ sbom (dict): Input SBOM with vulnerabilities
+ cve_linux (CVE_LINUX): CVE_LINUX instance for analysis
+ cc_path (str): Path to compile_commands.json
+ """
+ # Load compiled files
+ compiled_files = cvecheck.CVE_LINUX.load_compiled_files(cc_path)
+ if not compiled_files:
+ print("Error: Failed to load compiled files", file=sys.stderr)
+ return False
+
+ # Filter kernel CVEs
+ analyze_kernel_cves(sbom, cve_linux, compiled_files)
+ return True
+
+
def main():
- parser = argparse.ArgumentParser(description=DESCRIPTION)
+ parser = argparse.ArgumentParser(
+ description=DESCRIPTION,
+ formatter_class=argparse.RawDescriptionHelpFormatter
+ )
parser.add_argument("-i", "--in-file", nargs="?", type=argparse.FileType("r"),
default=(None if sys.stdin.isatty() else sys.stdin))
parser.add_argument("-o", "--out-file", nargs="?", type=argparse.FileType("w"),
@@ -297,8 +378,15 @@ def main():
parser.add_argument("--include-resolved", default=False, action='store_true',
help="Add vulnerabilities already 'resolved' that don't affect a " +
"component to the output CycloneDX vulnerabilities analysis.")
- parser.add_argument("--no-nvd-update", default=False, action='store_true',
- help="Doesn't update the NVD database.")
+ parser.add_argument('--cc-path', dest='cc_path', default=None,
+ help="Path to the kernel_compile_commands.json file for CVE filtering. " +
+ "Marks non-compiled kernel CVEs as 'not_affected' with 'code_not_present' justification. " +
+ "Requires --cna-path. " +
+ "Requires BR2_LINUX_KERNEL_COMPILE_COMMANDS to be enabled.")
+ parser.add_argument('--cna-path', dest='cna_path', default=None,
+ help='Path to CNA database (cvelistV5) for kernel CVE filtering')
+ parser.add_argument("--no-db-update", default=False, action='store_true',
+ help="Doesn't clone/pull the CVE databases (NVD/CNA).")
args = parser.parse_args()
@@ -306,21 +394,42 @@ def main():
parser.print_help()
sys.exit(1)
+ cve_linux = None
+ if args.cc_path:
+ if args.cna_path is None:
+ print("ERROR: cna_path is required when cc_path is specified!", file=sys.stderr)
+ parser.print_help()
+ sys.exit(1)
+ else:
+ # Initialize CVE_LINUX for kernel CVE analysis
+ cve_linux = cvecheck.CVE_LINUX(args.cna_path)
+
sbom = json.load(args.in_file)
opt = Options(
include_resolved=args.include_resolved,
)
+ # Sync NVD database if requested
args.nvd_path.mkdir(parents=True, exist_ok=True)
- if not args.no_nvd_update:
+ if not args.no_db_update:
cvecheck.CVE.download_nvd(args.nvd_path)
+ # Sync CNA database if requested
+ if cve_linux and not args.no_db_update:
+ cve_linux.sync_database()
+
+ # Process
if args.enrich_only:
enrich_vulnerabilities(args.nvd_path, sbom)
else:
check_package_cves(args.nvd_path, sbom, opt)
+ # Apply kernel CVE filtering if enabled
+ if cve_linux:
+ apply_kernel_cve_filtering(sbom, cve_linux, args.cc_path)
+
+ # write results
args.out_file.write(json.dumps(sbom, indent=2))
args.out_file.write('\n')
diff --git a/support/scripts/cve.py b/support/scripts/cve.py
index 3875c4258c..4aaa978302 100755
--- a/support/scripts/cve.py
+++ b/support/scripts/cve.py
@@ -24,11 +24,14 @@ import json
import subprocess
import sys
import operator
+from pathlib import Path
+from typing import Optional, Set, List
sys.path.append('utils/')
NVD_START_YEAR = 1999
-NVD_BASE_URL = "https://github.com/fkie-cad/nvd-json-data-feeds/"
+NVD_BASE_URL = "https://github.com/fkie-cad/nvd-json-data-feeds.git"
+CNA_REPO_URL = "https://github.com/CVEProject/cvelistV5.git"
ops = {
'>=': operator.ge,
@@ -39,6 +42,67 @@ ops = {
}
+class CVEDatabase:
+ """Generic class for managing CVE database operations (clone, pull, sync)"""
+
+ def __init__(self, repo_url: str, db_dir: str):
+ """
+ Initialize CVE database manager.
+
+ Args:
+ repo_url (str): Git repository URL for the CVE database
+ db_dir (str): Local directory path for the database
+ """
+ self.repo_url = repo_url
+ self.db_dir = db_dir
+
+ def exists(self) -> bool:
+ """Check if the database directory exists"""
+ return os.path.exists(self.db_dir)
+
+ def is_git_repo(self) -> bool:
+ """Check if the database directory is a git repository"""
+ return os.path.exists(os.path.join(self.db_dir, ".git"))
+
+ def sync(self) -> bool:
+ """
+ Clone or update the CVE database from GitHub.
+
+ If the directory doesn't exist, clone the repository.
+ If it exists and is a git repository, pull the latest changes.
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ if self.is_git_repo():
+ # Directory is a git repo, pull latest changes
+ subprocess.run(
+ ["git", "pull"],
+ cwd=self.db_dir,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ )
+ return True
+ else:
+ # Clone the repository
+ os.makedirs(self.db_dir, exist_ok=True)
+ subprocess.run(
+ ["git", "clone", self.repo_url, self.db_dir],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ )
+ return True
+ except subprocess.CalledProcessError:
+ print(f"Warning: Failed to sync database from {self.repo_url}", file=sys.stderr)
+ return False
+ except FileNotFoundError:
+ print("Warning: git is not installed or not in PATH", file=sys.stderr)
+ return False
+
+
class CPE:
DISJOINT = 0
SUBSET = 1
@@ -145,24 +209,9 @@ class CVE:
@staticmethod
def download_nvd(nvd_dir):
- nvd_git_dir = os.path.join(nvd_dir, "git")
-
- if os.path.exists(nvd_git_dir):
- subprocess.check_call(
- ["git", "pull"],
- cwd=nvd_git_dir,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- )
- else:
- # Create the directory and its parents; git
- # happily clones into an empty directory.
- os.makedirs(nvd_git_dir)
- subprocess.check_call(
- ["git", "clone", NVD_BASE_URL, nvd_git_dir],
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- )
+ """Download or update the NVD database"""
+ db = CVEDatabase(NVD_BASE_URL, nvd_dir)
+ db.sync()
@staticmethod
def sort_id(cve_ids):
@@ -178,10 +227,8 @@ class CVE:
feeds since NVD_START_YEAR. If the files are missing or outdated in
nvd_dir, a fresh copy will be downloaded, and kept in .json.gz
"""
- nvd_git_dir = os.path.join(nvd_dir, "git")
-
for year in range(NVD_START_YEAR, datetime.datetime.now().year + 1):
- for dirpath, _, filenames in os.walk(os.path.join(nvd_git_dir, f"CVE-{year}")):
+ for dirpath, _, filenames in os.walk(os.path.join(nvd_dir, f"CVE-{year}")):
for filename in filenames:
if filename[-5:] != ".json":
continue
@@ -340,3 +387,206 @@ class CVE:
return self.CVE_AFFECTS
return self.CVE_DOESNT_AFFECT
+
+
+class CVE_LINUX:
+ """Specialized class for Linux kernel CVE analysis based on compiled files.
+
+ Uses the CNA (CVE Numbering Authority) database (cvelistV5) to determine
+ if CVEs affecting the Linux kernel are applicable to the current build
+ based on which files are actually compiled.
+ """
+
+ CVE_APPLICABLE = 1 # CVE affects compiled files
+ CVE_NOT_APPLICABLE = 2 # CVE doesn't affect any compiled files
+ CVE_INSUFFICIENT_DATA = 3 # CVE not found in CNA or no program files info
+
+ def __init__(self, cna_dir: str):
+ """Initialize with path to CNA database (cvelistV5)"""
+ self.cna_dir = cna_dir
+ self.db = CVEDatabase(CNA_REPO_URL, cna_dir)
+
+ def sync_database(self) -> bool:
+ """Sync (clone or pull) the CNA database from GitHub"""
+ return self.db.sync()
+
+ @staticmethod
+ def load_compiled_files(compile_commands_path: str) -> Set[str]:
+ """
+ Load compile_commands.json and extract the list of compiled files.
+ Returns a set of relative paths (e.g., 'drivers/net/foo.c').
+ """
+ p = Path(compile_commands_path)
+ if not p.exists():
+ print(f"Error: compile_commands.json not found: {compile_commands_path}", file=sys.stderr)
+ return set()
+
+ try:
+ with open(p, encoding="utf-8") as f:
+ commands = json.load(f)
+ except (json.JSONDecodeError, OSError) as e:
+ print(f"Error: Failed to load compile_commands.json: {e}", file=sys.stderr)
+ return set()
+
+ if not isinstance(commands, list):
+ print("Error: Unexpected format in compile_commands.json", file=sys.stderr)
+ return set()
+
+ # Infer kernel_dir from compile_commands
+ kernel_dir = None
+ if commands:
+ first_dir = Path(commands[0].get("directory", ""))
+ kernel_dir = str(first_dir.resolve())
+
+ kernel_root = Path(kernel_dir).resolve() if kernel_dir else None
+ compiled_files = set()
+
+ for entry in commands:
+ file_path = Path(entry.get("file", ""))
+
+ # Make relative to kernel_root if possible
+ if kernel_root:
+ try:
+ rel_path = file_path.resolve().relative_to(kernel_root)
+ compiled_files.add(str(rel_path))
+ except ValueError:
+ # File outside kernel tree → ignore
+ continue
+ else:
+ # Fallback: use path as-is
+ compiled_files.add(str(file_path))
+
+ return compiled_files
+
+ def load_cve_from_cna(self, cve_id: str) -> Optional[dict]:
+ """
+ Load a specific CVE from the CNA directory.
+
+ Searches for a file named CVE-YYYY-NNNNN.json or similar.
+ Returns a dict with extracted info, or None if not found/invalid.
+ """
+ cna_path = Path(self.cna_dir)
+
+ # Construct possible file names
+ possible_names = [
+ f"{cve_id}.json",
+ f"{cve_id.replace('CVE-', '')}.json",
+ ]
+
+ cve_file = None
+ for name in possible_names:
+ candidate = cna_path / name
+ if candidate.exists():
+ cve_file = candidate
+ break
+
+ # Recursive search as last resort
+ if not cve_file:
+ matches = list(cna_path.rglob(f"{cve_id}.json"))
+ if matches:
+ cve_file = matches[0]
+
+ if not cve_file:
+ return None
+
+ try:
+ with open(cve_file, encoding="utf-8") as f:
+ data = json.load(f)
+ except (json.JSONDecodeError, OSError):
+ return None
+
+ # Verify CNA 5.x format
+ if data.get("dataType") != "CVE_RECORD":
+ return None
+
+ cna = data.get("containers", {}).get("cna", {})
+ if not cna:
+ return None
+
+ # Extract programFiles
+ program_files = []
+ for affected in cna.get("affected", []):
+ if affected.get("vendor") == "Linux" and affected.get("product") == "Linux":
+ program_files.extend(affected.get("programFiles", []))
+
+ # Deduplicate
+ program_files = sorted(set(program_files))
+
+ return {
+ "cve_id": cve_id,
+ "program_files": program_files,
+ }
+
+ @staticmethod
+ def match_files(program_files: List[str], compiled_files: Set[str]) -> List[str]:
+ """
+ Return compiled files that match the CVE's programFiles.
+ Uses suffix matching to handle path differences.
+ """
+ matched = []
+
+ for prog_file in program_files:
+ # Normalize: remove leading slash
+ prog_norm = prog_file.lstrip("/")
+
+ for compiled in compiled_files:
+ # Exact match
+ if compiled == prog_norm:
+ if compiled not in matched:
+ matched.append(compiled)
+ break
+
+ return sorted(matched)
+
+ def affects(self, cve_id: str, compiled_files: Set[str]) -> int:
+ """
+ Determine if a Linux kernel CVE affects the current build.
+
+ Returns:
+ CVE_APPLICABLE: CVE affects compiled files
+ CVE_NOT_APPLICABLE: CVE doesn't affect any compiled files
+ CVE_INSUFFICIENT_DATA: CVE not found in CNA or no program files info
+ """
+ cve_cna = self.load_cve_from_cna(cve_id)
+
+ if not cve_cna:
+ # CVE not found in CNA database - keep for review (insufficient data)
+ return self.CVE_INSUFFICIENT_DATA
+
+ if not cve_cna["program_files"]:
+ # No program files info - keep for review
+ return self.CVE_INSUFFICIENT_DATA
+
+ # Check if any affected files are compiled
+ matched = self.match_files(cve_cna["program_files"], compiled_files)
+
+ if len(matched) > 0:
+ return self.CVE_APPLICABLE
+ else:
+ return self.CVE_NOT_APPLICABLE
+
+ def get_affected_files(self, cve_id: str, compiled_files: Set[str]) -> dict:
+ """
+ Get details about which files are affected by a CVE and which are compiled.
+
+ Returns a dict with:
+ - cve_id: The CVE identifier
+ - program_files: All affected program files from CNA
+ - matched_compiled: Compiled files that match affected files
+ """
+ cve_cna = self.load_cve_from_cna(cve_id)
+
+ if not cve_cna:
+ return {
+ "cve_id": cve_id,
+ "program_files": [],
+ "matched_compiled": [],
+ }
+
+ matched = self.match_files(cve_cna["program_files"], compiled_files)
+
+ return {
+ "cve_id": cve_id,
+ "program_files": cve_cna["program_files"],
+ "matched_compiled": matched,
+ }
--
2.43.0
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files
2026-02-25 10:56 ` [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files Fabien Lehoussel via buildroot
@ 2026-02-27 9:50 ` Thomas Perale via buildroot
0 siblings, 0 replies; 4+ messages in thread
From: Thomas Perale via buildroot @ 2026-02-27 9:50 UTC (permalink / raw)
To: Fabien Lehoussel; +Cc: Thomas Perale, buildroot
Hi Fabien,
In reply of:
> Introduce optional filtering for Linux kernel CVEs using compile_commands.json
> and the CNA database [1].
> This reduces false positives by marking CVEs that don't
> affect compiled files as "not_affected" with "code_not_present" justification,
> while reporting only CVEs that affect compiled files, enhancing the relevance
> of CVE reports for specific builds.
>
> Changes:
> - Add CVEDatabase class for generic CVE database management (clone/pull)
> supporting both NVD and CNA databases with automatic sync capability
> Database path argument is the repository root directly (no automatic 'git' subfolder)
> enabling seamless CI/CD integration and clearer path semantics
> - Add CVE_LINUX class for specialized Linux kernel CVE analysis
> - Load compiled files from compile_commands.json
> - Match CVE affected files from CNA database against compiled files
> - Categorize CVEs as applicable, not applicable, or insufficient data
> - Enhance cve-check script with optional kernel CVE filtering
> - Activate filtering automatically when --cc-path and --cna-path are provided
> - Replace --no-nvd-update with generic --no-db-update flag for both databases
> - Mark non-compiled kernel CVEs as "not_affected" with "code_not_present" justification
> - Preserve audit trail by keeping all vulnerabilities in SBOM
> - Keeps potentially applicable and uncertain Linux CVEs for manual review
>
> Usage:
> make show-info | utils/generate-cyclonedx | support/script/cve-check \
> --nvd-path dl/buildroot-nvd/ \
> --cc-path output/images/linux_compile_commands.json \
> --cna-path dl/buildroot-cvelistV5 \
> --no-db-update
>
> Requires:
> - linux_compile_commands.json from kernel build (BR2_LINUX_KERNEL_COMPILE_COMMANDS)
>
> [1] CNA database: https://github.com/CVEProject/cvelistV5.git
>
> Signed-off-by: Fabien Lehoussel <fabien.lehoussel@smile.fr>
> ---
> support/scripts/cve-check | 119 ++++++++++++++-
> support/scripts/cve.py | 294 +++++++++++++++++++++++++++++++++++---
> 2 files changed, 386 insertions(+), 27 deletions(-)
>
> diff --git a/support/scripts/cve-check b/support/scripts/cve-check
> index ff14e4b238..1e3743248a 100755
> --- a/support/scripts/cve-check
> +++ b/support/scripts/cve-check
> @@ -7,11 +7,20 @@
> # The NVD database is cloned using a mirror of it and the content is compared
> # locally.
> #
> +# For Linux kernel CVEs, optionally filters them based on compiled files using
> +# the CNA database (cvelistV5) and compile_commands.json matching.
> +#
> # Example usage:
> # $ make show-info | utils/generate-cyclonedx | support/script/cve-check --nvd-path dl/buildroot-nvd/
> +#
> +# With kernel CVE filtering:
> +# $ make show-info | utils/generate-cyclonedx | support/script/cve-check \
> +# --nvd-path dl/buildroot-nvd/ \
> +# --cc-path output/images/linux_compile_commands.json \
> +# --cna-path dl/buildroot-cvelistV5
> from collections import defaultdict
> from pathlib import Path
> -from typing import TypedDict
> +from typing import TypedDict, Optional
> import argparse
> import sys
> import json
> @@ -29,6 +38,16 @@ database.
>
> The NVD database is cloned using a mirror of it and the content is compared
> locally.
> +
> +For Linux kernel CVEs, optionally filters them based on compiled files using
> +the CNA database (cvelistV5) and compile_commands.json matching.
> +
> +When kernel CVE filtering is enabled (via --cc-path and --cna-path):
> +- Marks false positives as "not_affected": CVEs where NO affected files are compiled
> + with "code_not_present" justification, preserving complete audit trail
> +- Keeps potentially applicable CVEs: where affected files ARE compiled
> +- Keeps uncertain CVEs: where CNA database lacks sufficient information
> +
> """
>
>
> @@ -281,8 +300,70 @@ def enrich_vulnerabilities(nvd_path: Path, sbom):
> vuln_append_or_update_affects_if_exists(vulnerabilities, vulnerability)
>
>
> +def analyze_kernel_cves(sbom, cve_linux: cvecheck.CVE_LINUX, compiled_files: set):
> + """
> + Analyze Linux kernel CVEs based on compiled files.
> +
> + Marks CVEs where NO affected files are compiled as "not_affected" with
> + "code_not_present" justification, preserving complete audit trail.
> + Keeps CVEs where affected files ARE compiled or insufficient data.
> +
> + Args:
> + sbom (dict): Input SBOM with vulnerabilities.
> + cve_linux (CVE_LINUX): CVE_LINUX instance for analysis.
> + compiled_files (set): Set of compiled kernel files.
> + """
> + vulnerabilities = sbom.get("vulnerabilities", [])
> +
> + # Process vulnerabilities: mark non-applicable kernel CVEs as not_affected
> + for vuln in vulnerabilities:
> + cve_id = vuln.get("id", "")
> +
> + # Check if this CVE affects "linux" component
> + affects = vuln.get("affects", [])
> + affects_linux = any(a.get("ref") == "linux" for a in affects)
> +
> + if not affects_linux or not cve_id.startswith("CVE-"):
> + # Not a kernel CVE, leave it unchanged
> + continue
> +
> + # Check if kernel CVE is applicable
> + status = cve_linux.affects(cve_id, compiled_files)
> +
> + if status == cvecheck.CVE_LINUX.CVE_NOT_APPLICABLE:
> + # Mark as not_affected since code is not present
> + vuln["analysis"] = {
> + "state": "not_affected",
> + "justification": "code_not_present"
> + }
> +
> + sbom["vulnerabilities"] = vulnerabilities
> +
> +def apply_kernel_cve_filtering(sbom, cve_linux, cc_path):
> + """
> + Apply kernel CVE filtering based on compiled files.
> +
> + Args:
> + sbom (dict): Input SBOM with vulnerabilities
> + cve_linux (CVE_LINUX): CVE_LINUX instance for analysis
> + cc_path (str): Path to compile_commands.json
> + """
> + # Load compiled files
> + compiled_files = cvecheck.CVE_LINUX.load_compiled_files(cc_path)
> + if not compiled_files:
> + print("Error: Failed to load compiled files", file=sys.stderr)
> + return False
> +
> + # Filter kernel CVEs
> + analyze_kernel_cves(sbom, cve_linux, compiled_files)
> + return True
> +
> +
> def main():
> - parser = argparse.ArgumentParser(description=DESCRIPTION)
> + parser = argparse.ArgumentParser(
> + description=DESCRIPTION,
> + formatter_class=argparse.RawDescriptionHelpFormatter
> + )
> parser.add_argument("-i", "--in-file", nargs="?", type=argparse.FileType("r"),
> default=(None if sys.stdin.isatty() else sys.stdin))
> parser.add_argument("-o", "--out-file", nargs="?", type=argparse.FileType("w"),
> @@ -297,8 +378,15 @@ def main():
> parser.add_argument("--include-resolved", default=False, action='store_true',
> help="Add vulnerabilities already 'resolved' that don't affect a " +
> "component to the output CycloneDX vulnerabilities analysis.")
> - parser.add_argument("--no-nvd-update", default=False, action='store_true',
> - help="Doesn't update the NVD database.")
> + parser.add_argument('--cc-path', dest='cc_path', default=None,
> + help="Path to the kernel_compile_commands.json file for CVE filtering. " +
> + "Marks non-compiled kernel CVEs as 'not_affected' with 'code_not_present' justification. " +
> + "Requires --cna-path. " +
> + "Requires BR2_LINUX_KERNEL_COMPILE_COMMANDS to be enabled.")
Why not default to `default=brpath / 'dl' / 'buildroot-cna'` similarly to `--nvd-path` ?
> + parser.add_argument('--cna-path', dest='cna_path', default=None,
> + help='Path to CNA database (cvelistV5) for kernel CVE filtering')
> + parser.add_argument("--no-db-update", default=False, action='store_true',
> + help="Doesn't clone/pull the CVE databases (NVD/CNA).")
Would do the name change in a seperate commit.
>
> args = parser.parse_args()
>
> @@ -306,21 +394,42 @@ def main():
> parser.print_help()
> sys.exit(1)
>
> + cve_linux = None
> + if args.cc_path:
> + if args.cna_path is None:
> + print("ERROR: cna_path is required when cc_path is specified!", file=sys.stderr)
> + parser.print_help()
> + sys.exit(1)
> + else:
> + # Initialize CVE_LINUX for kernel CVE analysis
> + cve_linux = cvecheck.CVE_LINUX(args.cna_path)
> +
> sbom = json.load(args.in_file)
>
> opt = Options(
> include_resolved=args.include_resolved,
> )
>
> + # Sync NVD database if requested
> args.nvd_path.mkdir(parents=True, exist_ok=True)
> - if not args.no_nvd_update:
> + if not args.no_db_update:
> cvecheck.CVE.download_nvd(args.nvd_path)
>
> + # Sync CNA database if requested
> + if cve_linux and not args.no_db_update:
> + cve_linux.sync_database()
> +
> + # Process
> if args.enrich_only:
> enrich_vulnerabilities(args.nvd_path, sbom)
> else:
> check_package_cves(args.nvd_path, sbom, opt)
>
> + # Apply kernel CVE filtering if enabled
> + if cve_linux:
> + apply_kernel_cve_filtering(sbom, cve_linux, args.cc_path)
> +
> + # write results
> args.out_file.write(json.dumps(sbom, indent=2))
> args.out_file.write('\n')
>
> diff --git a/support/scripts/cve.py b/support/scripts/cve.py
> index 3875c4258c..4aaa978302 100755
> --- a/support/scripts/cve.py
> +++ b/support/scripts/cve.py
> @@ -24,11 +24,14 @@ import json
> import subprocess
> import sys
> import operator
> +from pathlib import Path
> +from typing import Optional, Set, List
>
> sys.path.append('utils/')
>
> NVD_START_YEAR = 1999
> -NVD_BASE_URL = "https://github.com/fkie-cad/nvd-json-data-feeds/"
> +NVD_BASE_URL = "https://github.com/fkie-cad/nvd-json-data-feeds.git"
> +CNA_REPO_URL = "https://github.com/CVEProject/cvelistV5.git"
>
> ops = {
> '>=': operator.ge,
> @@ -39,6 +42,67 @@ ops = {
> }
>
>
> +class CVEDatabase:
> + """Generic class for managing CVE database operations (clone, pull, sync)"""
> +
> + def __init__(self, repo_url: str, db_dir: str):
> + """
> + Initialize CVE database manager.
> +
> + Args:
> + repo_url (str): Git repository URL for the CVE database
> + db_dir (str): Local directory path for the database
> + """
> + self.repo_url = repo_url
> + self.db_dir = db_dir
> +
> + def exists(self) -> bool:
> + """Check if the database directory exists"""
> + return os.path.exists(self.db_dir)
> +
> + def is_git_repo(self) -> bool:
> + """Check if the database directory is a git repository"""
> + return os.path.exists(os.path.join(self.db_dir, ".git"))
> +
> + def sync(self) -> bool:
> + """
> + Clone or update the CVE database from GitHub.
> +
> + If the directory doesn't exist, clone the repository.
> + If it exists and is a git repository, pull the latest changes.
> +
> + Returns:
> + bool: True if successful, False otherwise
> + """
> + try:
> + if self.is_git_repo():
> + # Directory is a git repo, pull latest changes
> + subprocess.run(
> + ["git", "pull"],
> + cwd=self.db_dir,
> + stdout=subprocess.DEVNULL,
> + stderr=subprocess.DEVNULL,
> + check=True,
> + )
> + return True
> + else:
> + # Clone the repository
> + os.makedirs(self.db_dir, exist_ok=True)
> + subprocess.run(
> + ["git", "clone", self.repo_url, self.db_dir],
> + stdout=subprocess.DEVNULL,
> + stderr=subprocess.DEVNULL,
> + check=True,
> + )
> + return True
> + except subprocess.CalledProcessError:
> + print(f"Warning: Failed to sync database from {self.repo_url}", file=sys.stderr)
> + return False
> + except FileNotFoundError:
> + print("Warning: git is not installed or not in PATH", file=sys.stderr)
> + return False
> +
> +
I would create this class in a seperate commit to really have the code specific
to the kernel CVE logic in a smaller commit.
> class CPE:
> DISJOINT = 0
> SUBSET = 1
> @@ -145,24 +209,9 @@ class CVE:
>
> @staticmethod
> def download_nvd(nvd_dir):
> - nvd_git_dir = os.path.join(nvd_dir, "git")
> -
> - if os.path.exists(nvd_git_dir):
> - subprocess.check_call(
> - ["git", "pull"],
> - cwd=nvd_git_dir,
> - stdout=subprocess.DEVNULL,
> - stderr=subprocess.DEVNULL,
> - )
> - else:
> - # Create the directory and its parents; git
> - # happily clones into an empty directory.
> - os.makedirs(nvd_git_dir)
> - subprocess.check_call(
> - ["git", "clone", NVD_BASE_URL, nvd_git_dir],
> - stdout=subprocess.DEVNULL,
> - stderr=subprocess.DEVNULL,
> - )
> + """Download or update the NVD database"""
> + db = CVEDatabase(NVD_BASE_URL, nvd_dir)
> + db.sync()
>
Maybe it's a good occasion to re-think the CVE class and instead move some of
the `staticmethod` present here into some kind of DB object that get inherited
by `NVDDB` & `CNADB`.
I don't know yet if it's possible (or even make sense) but I think it would
make sense to be able to compare packages (not only kernel) by both the NVD &
CNA Db.
I'm thinking about the `read_nvd_dir` method which should probably have some
code in common with `load_cve_from_cna`.
> @staticmethod
> def sort_id(cve_ids):
> @@ -178,10 +227,8 @@ class CVE:
> feeds since NVD_START_YEAR. If the files are missing or outdated in
> nvd_dir, a fresh copy will be downloaded, and kept in .json.gz
> """
> - nvd_git_dir = os.path.join(nvd_dir, "git")
> -
> for year in range(NVD_START_YEAR, datetime.datetime.now().year + 1):
> - for dirpath, _, filenames in os.walk(os.path.join(nvd_git_dir, f"CVE-{year}")):
> + for dirpath, _, filenames in os.walk(os.path.join(nvd_dir, f"CVE-{year}")):
> for filename in filenames:
> if filename[-5:] != ".json":
> continue
> @@ -340,3 +387,206 @@ class CVE:
> return self.CVE_AFFECTS
>
> return self.CVE_DOESNT_AFFECT
> +
> +
> +class CVE_LINUX:
> + """Specialized class for Linux kernel CVE analysis based on compiled files.
> +
> + Uses the CNA (CVE Numbering Authority) database (cvelistV5) to determine
> + if CVEs affecting the Linux kernel are applicable to the current build
> + based on which files are actually compiled.
> + """
> +
> + CVE_APPLICABLE = 1 # CVE affects compiled files
> + CVE_NOT_APPLICABLE = 2 # CVE doesn't affect any compiled files
> + CVE_INSUFFICIENT_DATA = 3 # CVE not found in CNA or no program files info
Those are similar to the CVE.CVE_AFFECT/CVE_DOESNT_AFFECT/CVE_UNKNOWN category
that already exists. It would make sense to find a way to put some stuff in
common.
> +
> + def __init__(self, cna_dir: str):
> + """Initialize with path to CNA database (cvelistV5)"""
> + self.cna_dir = cna_dir
> + self.db = CVEDatabase(CNA_REPO_URL, cna_dir)
> +
> + def sync_database(self) -> bool:
> + """Sync (clone or pull) the CNA database from GitHub"""
> + return self.db.sync()
> +
> + @staticmethod
> + def load_compiled_files(compile_commands_path: str) -> Set[str]:
> + """
> + Load compile_commands.json and extract the list of compiled files.
> + Returns a set of relative paths (e.g., 'drivers/net/foo.c').
> + """
> + p = Path(compile_commands_path)
> + if not p.exists():
> + print(f"Error: compile_commands.json not found: {compile_commands_path}", file=sys.stderr)
> + return set()
> +
> + try:
> + with open(p, encoding="utf-8") as f:
> + commands = json.load(f)
> + except (json.JSONDecodeError, OSError) as e:
> + print(f"Error: Failed to load compile_commands.json: {e}", file=sys.stderr)
> + return set()
> +
> + if not isinstance(commands, list):
> + print("Error: Unexpected format in compile_commands.json", file=sys.stderr)
> + return set()
> +
> + # Infer kernel_dir from compile_commands
> + kernel_dir = None
> + if commands:
> + first_dir = Path(commands[0].get("directory", ""))
> + kernel_dir = str(first_dir.resolve())
> +
> + kernel_root = Path(kernel_dir).resolve() if kernel_dir else None
> + compiled_files = set()
> +
> + for entry in commands:
> + file_path = Path(entry.get("file", ""))
> +
> + # Make relative to kernel_root if possible
> + if kernel_root:
> + try:
> + rel_path = file_path.resolve().relative_to(kernel_root)
> + compiled_files.add(str(rel_path))
> + except ValueError:
> + # File outside kernel tree → ignore
> + continue
> + else:
> + # Fallback: use path as-is
> + compiled_files.add(str(file_path))
> +
> + return compiled_files
> +
> + def load_cve_from_cna(self, cve_id: str) -> Optional[dict]:
> + """
> + Load a specific CVE from the CNA directory.
> +
> + Searches for a file named CVE-YYYY-NNNNN.json or similar.
> + Returns a dict with extracted info, or None if not found/invalid.
> + """
> + cna_path = Path(self.cna_dir)
> +
> + # Construct possible file names
> + possible_names = [
> + f"{cve_id}.json",
> + f"{cve_id.replace('CVE-', '')}.json",
> + ]
> +
> + cve_file = None
> + for name in possible_names:
> + candidate = cna_path / name
> + if candidate.exists():
> + cve_file = candidate
> + break
> +
> + # Recursive search as last resort
> + if not cve_file:
> + matches = list(cna_path.rglob(f"{cve_id}.json"))
> + if matches:
> + cve_file = matches[0]
> +
> + if not cve_file:
> + return None
> +
> + try:
> + with open(cve_file, encoding="utf-8") as f:
> + data = json.load(f)
> + except (json.JSONDecodeError, OSError):
> + return None
> +
> + # Verify CNA 5.x format
> + if data.get("dataType") != "CVE_RECORD":
> + return None
> +
> + cna = data.get("containers", {}).get("cna", {})
> + if not cna:
> + return None
> +
> + # Extract programFiles
> + program_files = []
> + for affected in cna.get("affected", []):
> + if affected.get("vendor") == "Linux" and affected.get("product") == "Linux":
> + program_files.extend(affected.get("programFiles", []))
> +
> + # Deduplicate
> + program_files = sorted(set(program_files))
> +
> + return {
> + "cve_id": cve_id,
> + "program_files": program_files,
> + }
> +
> + @staticmethod
> + def match_files(program_files: List[str], compiled_files: Set[str]) -> List[str]:
> + """
> + Return compiled files that match the CVE's programFiles.
> + Uses suffix matching to handle path differences.
> + """
> + matched = []
> +
> + for prog_file in program_files:
> + # Normalize: remove leading slash
> + prog_norm = prog_file.lstrip("/")
> +
> + for compiled in compiled_files:
> + # Exact match
> + if compiled == prog_norm:
> + if compiled not in matched:
> + matched.append(compiled)
> + break
> +
> + return sorted(matched)
> +
> + def affects(self, cve_id: str, compiled_files: Set[str]) -> int:
> + """
> + Determine if a Linux kernel CVE affects the current build.
> +
> + Returns:
> + CVE_APPLICABLE: CVE affects compiled files
> + CVE_NOT_APPLICABLE: CVE doesn't affect any compiled files
> + CVE_INSUFFICIENT_DATA: CVE not found in CNA or no program files info
> + """
> + cve_cna = self.load_cve_from_cna(cve_id)
> +
> + if not cve_cna:
> + # CVE not found in CNA database - keep for review (insufficient data)
> + return self.CVE_INSUFFICIENT_DATA
> +
> + if not cve_cna["program_files"]:
> + # No program files info - keep for review
> + return self.CVE_INSUFFICIENT_DATA
> +
> + # Check if any affected files are compiled
> + matched = self.match_files(cve_cna["program_files"], compiled_files)
> +
> + if len(matched) > 0:
> + return self.CVE_APPLICABLE
> + else:
> + return self.CVE_NOT_APPLICABLE
> +
> + def get_affected_files(self, cve_id: str, compiled_files: Set[str]) -> dict:
> + """
> + Get details about which files are affected by a CVE and which are compiled.
> +
> + Returns a dict with:
> + - cve_id: The CVE identifier
> + - program_files: All affected program files from CNA
> + - matched_compiled: Compiled files that match affected files
> + """
> + cve_cna = self.load_cve_from_cna(cve_id)
> +
> + if not cve_cna:
> + return {
> + "cve_id": cve_id,
> + "program_files": [],
> + "matched_compiled": [],
> + }
> +
> + matched = self.match_files(cve_cna["program_files"], compiled_files)
> +
> + return {
> + "cve_id": cve_id,
> + "program_files": cve_cna["program_files"],
> + "matched_compiled": matched,
> + }
> --
> 2.43.0
>
> _______________________________________________
> buildroot mailing list
> buildroot@buildroot.org
> https://lists.buildroot.org/mailman/listinfo/buildroot
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-02-27 9:50 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-25 10:55 [Buildroot] [PATCH v3 0/2] Linux kernel CVE filtering Fabien Lehoussel via buildroot
2026-02-25 10:55 ` [Buildroot] [PATCH 1/2] linux/linux.mk: add generation of compile_commands.json Fabien Lehoussel via buildroot
2026-02-25 10:56 ` [Buildroot] [PATCH v3 2/2] support/scripts/cve-check: add kernel CVE filtering based on compiled files Fabien Lehoussel via buildroot
2026-02-27 9:50 ` Thomas Perale via buildroot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox