22
33from __future__ import annotations
44
5+ import re
56import shutil
67from abc import ABC , abstractmethod
78from typing import TYPE_CHECKING , Any , Literal
89
910import typer
11+ from packaging import version
1012from 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
1416if 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+
128163class 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
158208class GitRepo (DownloadableRequirement ):
0 commit comments