From 16dea9bf8fc9f2106617a044a306a4e848d912f1 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 8 Jan 2026 12:08:54 +0100 Subject: [PATCH 1/5] fix(utils): restore `cwd` as an optional argument for `run_command` --- codesectools/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codesectools/utils.py b/codesectools/utils.py index b8ed7dd..acae7f2 100644 --- a/codesectools/utils.py +++ b/codesectools/utils.py @@ -104,7 +104,7 @@ def render_command(command: list, mapping: dict[str, str]) -> list[str]: def run_command( - command: Sequence[str], cwd: Path, env: dict[str, str] | None = None + command: Sequence[str], cwd: Path | None = None, env: dict[str, str] | None = None ) -> tuple[int | None, str]: """Execute a command in a subprocess and capture its output. @@ -118,6 +118,8 @@ def run_command( stdout/stderr output as a string. """ + if cwd is None: + cwd = Path.cwd() modified_env = {**os.environ, **env} if env else os.environ process = subprocess.Popen( From 8a075d396dc7c0a732fe9e05bdf17202d816b66e Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 8 Jan 2026 12:11:57 +0100 Subject: [PATCH 2/5] feat(sasts): add `BinaryVersion` requirement --- codesectools/sasts/core/sast/requirements.py | 54 +++++++++++++++++++- pyproject.toml | 1 + requirements.txt | 4 +- uv.lock | 2 + 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/codesectools/sasts/core/sast/requirements.py b/codesectools/sasts/core/sast/requirements.py index 3d4a77a..816aa00 100644 --- a/codesectools/sasts/core/sast/requirements.py +++ b/codesectools/sasts/core/sast/requirements.py @@ -2,14 +2,16 @@ from __future__ import annotations +import re import shutil from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Literal import typer +from packaging import version from rich import print -from codesectools.utils import USER_CACHE_DIR, USER_CONFIG_DIR +from codesectools.utils import USER_CACHE_DIR, USER_CONFIG_DIR, run_command if TYPE_CHECKING: from pathlib import Path @@ -125,6 +127,39 @@ def is_fulfilled(self, **kwargs: Any) -> bool: return (USER_CONFIG_DIR / self.sast_name / self.name).is_file() +class BinaryVersion: + """Represent a version requirement for a binary.""" + + def __init__(self, command_flag: str, pattern: str, expected: str) -> None: + """Initialize a Version instance. + + Args: + command_flag: The command line flag to get the version string (e.g., '--version'). + pattern: A regex pattern to extract the version number from the output. + expected: The minimum expected version string. + + """ + self.command_flag = command_flag + self.pattern = pattern + self.expected = version.parse(expected) + + def check(self, binary: Binary) -> bool: + """Check if the binary's version meets the requirement. + + Args: + binary: The Binary requirement object to check. + + Returns: + True if the version is sufficient, False otherwise. + + """ + retcode, output = run_command([binary.name, self.command_flag]) + if m := re.search(self.pattern, output): + detected_version = version.parse(m.group(1)) + return detected_version >= self.expected + return False + + class Binary(SASTRequirement): """Represent a binary executable requirement for a SAST tool.""" @@ -132,6 +167,7 @@ def __init__( self, name: str, depends_on: list[SASTRequirement] | None = None, + version: BinaryVersion | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -141,6 +177,7 @@ def __init__( Args: name: The name of the requirement. depends_on: A list of other requirements that must be fulfilled first. + version: An optional BinaryVersion object to check for a minimum version. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. doc: A flag indicating if the instruction is available in the documentation. @@ -149,10 +186,23 @@ def __init__( super().__init__( name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc ) + self.version = version + + def __repr__(self) -> str: + """Return a developer-friendly string representation of the requirement.""" + if self.version: + return f"{self.__class__.__name__}({self.name}>={self.version.expected})" + else: + return super().__repr__() def is_fulfilled(self, **kwargs: Any) -> bool: """Check if the binary is available in the system's PATH.""" - return bool(shutil.which(self.name)) + if bool(shutil.which(self.name)): + if self.version: + return self.version.check(self) + return True + else: + return False class GitRepo(DownloadableRequirement): diff --git a/pyproject.toml b/pyproject.toml index 3511843..ba9c7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "lxml>=6.0.2", "matplotlib>=3.10.3", "numpy>=2.3.1", + "packaging>=25.0", "python-on-whales>=0.79.0", "pyyaml>=6.0.2", "requests>=2.32.4", diff --git a/requirements.txt b/requirements.txt index c2cc98b..4731f0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -437,7 +437,9 @@ numpy==2.3.5 \ packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f - # via matplotlib + # via + # codesectools + # matplotlib pillow==11.3.0 \ --hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \ --hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \ diff --git a/uv.lock b/uv.lock index 638544c..4431326 100644 --- a/uv.lock +++ b/uv.lock @@ -238,6 +238,7 @@ dependencies = [ { name = "lxml" }, { name = "matplotlib" }, { name = "numpy" }, + { name = "packaging" }, { name = "python-on-whales" }, { name = "pyyaml" }, { name = "requests" }, @@ -292,6 +293,7 @@ requires-dist = [ { name = "mkdocs-open-in-new-tab", marker = "extra == 'docs'", specifier = ">=1.0.8" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.30.0" }, { name = "numpy", specifier = ">=2.3.1" }, + { name = "packaging", specifier = ">=25.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.1" }, { name = "pytest-order", marker = "extra == 'test'", specifier = ">=1.3.0" }, From aa2fe2fb11479ffa160ff2a946ccdd278d33dc74 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 8 Jan 2026 12:14:34 +0100 Subject: [PATCH 3/5] fix(cppcheck): enforce minimum version 2.16.0 SARIF output format is only supported in CppCheck versions 2.16.0 and later. --- codesectools/sasts/tools/Cppcheck/sast.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codesectools/sasts/tools/Cppcheck/sast.py b/codesectools/sasts/tools/Cppcheck/sast.py index 600fab0..4b28b79 100644 --- a/codesectools/sasts/tools/Cppcheck/sast.py +++ b/codesectools/sasts/tools/Cppcheck/sast.py @@ -10,6 +10,7 @@ from codesectools.sasts.core.sast.properties import SASTProperties from codesectools.sasts.core.sast.requirements import ( Binary, + BinaryVersion, SASTRequirements, ) from codesectools.sasts.tools.Cppcheck.parser import CppcheckAnalysisResult @@ -40,7 +41,11 @@ class CppcheckSAST(PrebuiltBuildlessSAST): properties = SASTProperties(free=True, offline=True) requirements = SASTRequirements( full_reqs=[ - Binary("cppcheck", url="https://cppcheck.sourceforge.io/"), + Binary( + "cppcheck", + url="https://cppcheck.sourceforge.io/", + version=BinaryVersion("--version", r"(\d+\.\d+\.\d+)", "2.16.0"), + ), ], partial_reqs=[], ) From 7ab579dfc77f03e8f681abd00e45bc80afb0fe6e Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 8 Jan 2026 12:40:40 +0100 Subject: [PATCH 4/5] ci(docker): switch CppCheck installation from apt to manual Prebuilt version of CppCheck does not meet the requirement (>=2.16.0) --- Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index c6865dd..b5fed2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,8 +28,7 @@ RUN apt update -qq && \ cloc \ openjdk-17-jdk-headless maven \ build-essential bear \ - -y -qq --no-install-recommends && \ - rm -rf /var/lib/apt/lists/* + -y -qq --no-install-recommends RUN groupadd -g $GID codesectools && \ useradd -l -u $UID -g codesectools -m codesectools -s /bin/bash && \ @@ -60,9 +59,11 @@ RUN curl -sL https://github.com/spotbugs/spotbugs/releases/download/4.9.8/spotbu ENV PATH="/home/codesectools/sasts/spotbugs/bin:$PATH" # Cppcheck -RUN sudo apt update -qq && \ - DEBIAN_FRONTEND=noninteractive sudo apt install cppcheck -y -qq --no-install-recommends && \ - sudo rm -rf /var/lib/apt/lists/* +RUN sudo apt install -y -qq --no-install-recommends libpcre3-dev && \ + curl -sL https://github.com/danmar/cppcheck/archive/refs/tags/2.19.0.tar.gz | tar -xzvf - && \ + mv cppcheck-* /home/codesectools/sasts/cppcheck && \ + (cd /home/codesectools/sasts/cppcheck && make -j$(nproc) MATCHCOMPILER=yes HAVE_RULES=yes CXXOPTS="-O2" CPPOPTS="-DNDEBUG") +ENV PATH="/home/codesectools/sasts/cppcheck:$PATH" # =========================== CodeSecTools =========================== COPY --from=builder --chown=codesectools:codesectools /app /app From faeca089d0084bac5ce4dcf7ecc8914dce4f5f26 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 8 Jan 2026 12:41:44 +0100 Subject: [PATCH 5/5] docs(sasts): update supported version --- docs/sast/profiles/cppcheck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sast/profiles/cppcheck.yaml b/docs/sast/profiles/cppcheck.yaml index 906d97e..23db568 100644 --- a/docs/sast/profiles/cppcheck.yaml +++ b/docs/sast/profiles/cppcheck.yaml @@ -2,7 +2,7 @@ name: Cppcheck description: Cppcheck is a static analysis tool for C/C++ code. It provides unique code analysis to detect bugs and focuses on detecting undefined behaviour and dangerous coding constructs. The goal is to have very few false positives. Cppcheck is designed to be able to analyze your C/C++ code even if it has non-standard syntax (common in embedded projects). type: Data Flow Analysis (Compiled code) url: https://cppcheck.sourceforge.io/ -supported_version: 2.13.0 +supported_version: ">=2.16.0" supported_languages: - C/C++ legal: