Skip to content

Commit 8a075d3

Browse files
committed
feat(sasts): add BinaryVersion requirement
1 parent 16dea9b commit 8a075d3

File tree

4 files changed

+58
-3
lines changed

4 files changed

+58
-3
lines changed

codesectools/sasts/core/sast/requirements.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from __future__ import annotations
44

5+
import re
56
import shutil
67
from abc import ABC, abstractmethod
78
from typing import TYPE_CHECKING, Any, Literal
89

910
import typer
11+
from packaging import version
1012
from rich import print
1113

12-
from codesectools.utils import USER_CACHE_DIR, USER_CONFIG_DIR
14+
from codesectools.utils import USER_CACHE_DIR, USER_CONFIG_DIR, run_command
1315

1416
if TYPE_CHECKING:
1517
from pathlib import Path
@@ -125,13 +127,47 @@ def is_fulfilled(self, **kwargs: Any) -> bool:
125127
return (USER_CONFIG_DIR / self.sast_name / self.name).is_file()
126128

127129

130+
class BinaryVersion:
131+
"""Represent a version requirement for a binary."""
132+
133+
def __init__(self, command_flag: str, pattern: str, expected: str) -> None:
134+
"""Initialize a Version instance.
135+
136+
Args:
137+
command_flag: The command line flag to get the version string (e.g., '--version').
138+
pattern: A regex pattern to extract the version number from the output.
139+
expected: The minimum expected version string.
140+
141+
"""
142+
self.command_flag = command_flag
143+
self.pattern = pattern
144+
self.expected = version.parse(expected)
145+
146+
def check(self, binary: Binary) -> bool:
147+
"""Check if the binary's version meets the requirement.
148+
149+
Args:
150+
binary: The Binary requirement object to check.
151+
152+
Returns:
153+
True if the version is sufficient, False otherwise.
154+
155+
"""
156+
retcode, output = run_command([binary.name, self.command_flag])
157+
if m := re.search(self.pattern, output):
158+
detected_version = version.parse(m.group(1))
159+
return detected_version >= self.expected
160+
return False
161+
162+
128163
class Binary(SASTRequirement):
129164
"""Represent a binary executable requirement for a SAST tool."""
130165

131166
def __init__(
132167
self,
133168
name: str,
134169
depends_on: list[SASTRequirement] | None = None,
170+
version: BinaryVersion | None = None,
135171
instruction: str | None = None,
136172
url: str | None = None,
137173
doc: bool = False,
@@ -141,6 +177,7 @@ def __init__(
141177
Args:
142178
name: The name of the requirement.
143179
depends_on: A list of other requirements that must be fulfilled first.
180+
version: An optional BinaryVersion object to check for a minimum version.
144181
instruction: A short instruction on how to download the requirement.
145182
url: A URL for more detailed instructions.
146183
doc: A flag indicating if the instruction is available in the documentation.
@@ -149,10 +186,23 @@ def __init__(
149186
super().__init__(
150187
name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc
151188
)
189+
self.version = version
190+
191+
def __repr__(self) -> str:
192+
"""Return a developer-friendly string representation of the requirement."""
193+
if self.version:
194+
return f"{self.__class__.__name__}({self.name}>={self.version.expected})"
195+
else:
196+
return super().__repr__()
152197

153198
def is_fulfilled(self, **kwargs: Any) -> bool:
154199
"""Check if the binary is available in the system's PATH."""
155-
return bool(shutil.which(self.name))
200+
if bool(shutil.which(self.name)):
201+
if self.version:
202+
return self.version.check(self)
203+
return True
204+
else:
205+
return False
156206

157207

158208
class GitRepo(DownloadableRequirement):

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"lxml>=6.0.2",
1616
"matplotlib>=3.10.3",
1717
"numpy>=2.3.1",
18+
"packaging>=25.0",
1819
"python-on-whales>=0.79.0",
1920
"pyyaml>=6.0.2",
2021
"requests>=2.32.4",

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,9 @@ numpy==2.3.5 \
437437
packaging==25.0 \
438438
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
439439
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
440-
# via matplotlib
440+
# via
441+
# codesectools
442+
# matplotlib
441443
pillow==11.3.0 \
442444
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
443445
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)