From 9e8ef5ae34038da3411b7c9c8f0db57c90258f02 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sat, 9 May 2026 19:56:25 +0300 Subject: [PATCH 01/13] fix(generate): handle missing or inaccessible files in man_parsing Fixes: #10 Signed-off-by: Andrei Carp --- .../heuristics/man_parsing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/attack_surface_approximation/dictionaries_generators/heuristics/man_parsing.py b/attack_surface_approximation/dictionaries_generators/heuristics/man_parsing.py index 54c9591..7f56505 100644 --- a/attack_surface_approximation/dictionaries_generators/heuristics/man_parsing.py +++ b/attack_surface_approximation/dictionaries_generators/heuristics/man_parsing.py @@ -21,17 +21,17 @@ def __get_arguments_from_manual( filter_func: typing.Callable, unescape: typing.Callable = None, ) -> typing.Generator[str, None, None]: - with gzip.open(filename, "rt") as manual: - try: + try: + with gzip.open(filename, "rt") as manual: content = manual.read() - except UnicodeDecodeError: - return + except (UnicodeDecodeError, FileNotFoundError, OSError): + return - if unescape: - content = unescape(content) + if unescape: + content = unescape(content) - arguments = filter_func(content) - yield from arguments + arguments = filter_func(content) + yield from arguments def generate(_: str = None) -> typing.List[str]: From 6754ef07489419937e878564e549e854a933107b Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sat, 9 May 2026 20:51:41 +0300 Subject: [PATCH 02/13] fix(generate): validate ELF path for binary_pattern_matching Added a check in the CLI to ensure the --elf option is provided when using binary_pattern_matching, preventing crashes and misleading results. Fixes: #12 Signed-off-by: Andrei Carp --- attack_surface_approximation/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/attack_surface_approximation/cli.py b/attack_surface_approximation/cli.py index 1d0984c..84939cc 100644 --- a/attack_surface_approximation/cli.py +++ b/attack_surface_approximation/cli.py @@ -54,6 +54,10 @@ def cli() -> None: ), ) def generate(heuristic: str, output: str, top: int, elf: str = None) -> None: + if heuristic == "binary_pattern_matching" and elf is None: + print("[ERROR] The 'binary_pattern_matching' heuristic requires an ELF file. Please provide one using the --elf option.") + return + generator = ArgumentsGenerator() generator.generate(heuristic, elf) arguments_count = generator.dump(output, top_count=top) From 447ae6b68a79d88cdfce6af5f20625da640b6d05 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sat, 9 May 2026 23:23:35 +0300 Subject: [PATCH 03/13] fix(deps): add local commons and update Docker SDK Added commons library as a local path dependency to fix ModuleNotFoundError and updated Docker SDK to 7.1.0 to support modern URL schemes. Fixes: #13, Fixes: #15 Signed-off-by: Andrei Carp --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d0d6b62..b35b3e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,10 @@ version = "0.1.0" python = "^3.12" pycparser = "^2.21" pyelftools = "^0.28" -docker = "^6.1.2" +docker = "==7.1.0" rich = "^12.5.1" click = "^8.1.3" +commons = {path = "../commons", develop = false} [tool.poetry.dev-dependencies] black = "^22.6.0" From 8e8d76df620b72e8ade40351f265fd089a57d7d1 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 00:24:04 +0300 Subject: [PATCH 04/13] fix(qbdi): use configured executable instead of hardcoded uname Replaced the hardcoded 'uname' command with the dynamic CONTAINER_EXECUTABLE from configuration. This ensures the fuzzer analyzes the intended binary. Signed-off-by: Andrei Carp --- .../arguments_fuzzing/qbdi_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py index 4b9b402..5aa7a85 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py @@ -153,11 +153,11 @@ def __build_analyze_command( stringified_arguments = argument.to_str() stdin_avoidance_command = "echo '\n' |" if timeout_retry else "" - return ( # TODO: {self.__configuration.CONTAINER_EXECUTABLE} + return ( f"timeout {self.timeout} sh -c " f"'{stdin_avoidance_command} LD_BIND_NOW=1 " "LD_PRELOAD=./libqbdi_tracer.so " - "uname " + f"{self.__configuration.CONTAINER_EXECUTABLE} " f"{stringified_arguments}'" ) From 6f82e073d748e51dc0a2710fe6cd24f5cb7b8f89 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 00:33:56 +0300 Subject: [PATCH 05/13] fix(tracer): prevent buffer underflow during baseline execution Added a check for argc > 1 before accessing command line arguments in the C tracer. This fixes intermittent crashes when running the binary without arguments during calibration. Signed-off-by: Andrei Carp --- .../qbdi_analysis_scripts/qbdi_preload_template.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c index fa04b9d..01349df 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c @@ -173,10 +173,14 @@ int qbdipreload_on_main(int argc, char **argv) { // Copy the arguments for (i = 1; i < argc; i++) { + if (strlen(command_line) + strlen(argv[i]) + 2 >= MAX_ARGS_LENGTH) + break; strcat(command_line, argv[i]); strcat(command_line, " "); } - command_line[strlen(command_line) - 1] = '\0'; + if (argc > 1 && strlen(command_line) > 0) { + command_line[strlen(command_line) - 1] = '\0'; + } return QBDIPRELOAD_NOT_HANDLED; } From 484306de9fde09e993b0daec082effea5b0d7b65 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 00:39:42 +0300 Subject: [PATCH 06/13] fix(tracer): fix heap corruption in memory segment tracking Introduced a separate counter for executable segments to avoid out-of-bounds writes. Previously, the global map index was used for a restricted array, causing SIGSEGV. Signed-off-by: Andrei Carp --- .../qbdi_analysis_scripts/qbdi_preload_template.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c index 01349df..6f4933a 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c @@ -81,7 +81,7 @@ char *encode_command_line(const unsigned char *command, size_t length) { static VMAction show_basic_block_callback(VMInstanceRef vm, const VMState* vmState, GPRState* gprState, FPRState* fprState, void* data) { size_t start_address, end_address; int abstract_address; - char parent_segment = -1, i; + int parent_segment = -1, i; // Check if the program reached main if (!start_trace) return QBDI_CONTINUE; @@ -102,6 +102,10 @@ static VMAction show_basic_block_callback(VMInstanceRef vm, const VMState* vmSta } } + // Safety check: if parent segment not found, skip this block + if (parent_segment == -1) + return QBDI_CONTINUE; + // Compute the abstract address start_address -= segments[parent_segment].start; abstract_address = (parent_segment << 24) + start_address; @@ -188,7 +192,7 @@ int qbdipreload_on_main(int argc, char **argv) { void get_segments() { qbdi_MemoryMap *maps; size_t maps_count; - int i; + int i, j = 0; // Get the memory maps maps = qbdi_getCurrentProcessMaps(false, &maps_count); @@ -206,8 +210,9 @@ void get_segments() { // Store the segments for (i = 0; i < maps_count; i++) { if (maps[i].permission >= QBDI_PF_EXEC && maps[i].end < MIN_MAPPED_ADDRESS) { - segments[i].start = maps[i].start; - segments[i].end = maps[i].end; + segments[j].start = maps[i].start; + segments[j].end = maps[i].end; + j++; } } } From 81309b3d73c54022d941083498aad6bfaad75bca Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 01:10:05 +0300 Subject: [PATCH 07/13] fix(tracer): prevent memory corruption during hash generation Migrated from stack allocation to dynamic allocation (malloc) for the hashed buffer and increased its size. This prevents stack corruption caused by buffer overflow when processing a large number of basic blocks. Signed-off-by: Andrei Carp --- .../qbdi_preload_template.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c index 6f4933a..291135e 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis_scripts/qbdi_preload_template.c @@ -17,6 +17,7 @@ #define BLOCKS_USED_IN_HASH 10000 #define MAX_ARGS_LENGTH 100 #define OUTPUT_FOLDER "results/" +#define HASH_BUF_SIZE (BLOCKS_USED_IN_HASH * 10) // Enough for 10000 hex ints /* Structures */ @@ -234,16 +235,19 @@ int qbdipreload_on_run(VMInstanceRef vm, rword start, rword stop) { int qbdipreload_on_exit(int status) { FILE *output_file; - char hashed[2 * BLOCKS_USED_IN_HASH * sizeof(int)] = {'\0'}; - char current_hash[2 * sizeof(int)]; + char *hashed = malloc(HASH_BUF_SIZE); + char current_hash[16]; char output_filename[2 * MAX_ARGS_LENGTH + sizeof(OUTPUT_FOLDER) + 1] = {'\0'}; int *p; int i = 0; - char uses_canaries_str; + + if (!hashed) return QBDIPRELOAD_NO_ERROR; + memset(hashed, 0, HASH_BUF_SIZE); // Create the string to be hashed for (p = (int*)utarray_front(blocks); p != NULL && i < BLOCKS_USED_IN_HASH; p = (int*)utarray_next(blocks, p), i++) { - sprintf(current_hash, "%x", *p); + int written = sprintf(current_hash, "%x", *p); + if (strlen(hashed) + written >= HASH_BUF_SIZE - 1) break; strcat(hashed, current_hash); } @@ -251,7 +255,11 @@ int qbdipreload_on_exit(int status) { strcat(output_filename, OUTPUT_FOLDER); strcat(output_filename, encode_command_line(command_line, strlen(command_line))); output_file = fopen(output_filename, "w"); - fprintf(output_file, "%d %ld %d", utarray_len(blocks), hash(hashed), uses_canaries); + if (output_file) { + fprintf(output_file, "%d %lu %d", utarray_len(blocks), hash(hashed), uses_canaries); + fclose(output_file); + } + free(hashed); return QBDIPRELOAD_NO_ERROR; } \ No newline at end of file From e16b84f6189e9e68e5a17d9557ad14bffbaa56ef Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 01:13:53 +0300 Subject: [PATCH 08/13] fix(qbdi): fix results accessibility between Docker and Host Added chmod calls to ensure result directories and files created by the root user in Docker are readable by the host Python process. Signed-off-by: Andrei Carp --- .../arguments_fuzzing/qbdi_analysis.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py index 5aa7a85..6e0ed30 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py @@ -121,6 +121,14 @@ def __create_container(self) -> None: f"sudo chmod 555 {self.__configuration.CONTAINER_EXECUTABLE}" ) + # Ensure the results directory is writable by everyone + self.__container.exec_run( + f"mkdir -p {self.__configuration.CONTAINER_RESULTS_FOLDER}" + ) + self.__container.exec_run( + f"sudo chmod 777 {self.__configuration.CONTAINER_RESULTS_FOLDER}" + ) + self.__container.exec_run( "cmake .", workdir=self.__configuration.CONTAINER_SO_FOLDER, @@ -187,7 +195,12 @@ def __run_analysis( raw_result = self.__build_and_run_analyze_command( argument, timeout_retry ) - print(raw_result.output) # TODO: remove + + # Ensure the result file is readable by the host user + argument_identifier = argument.to_hex_id() + self.__container.exec_run( + f"chmod 666 {os.path.join(self.__configuration.CONTAINER_RESULTS_FOLDER, argument_identifier)}" + ) result_filename = self.__get_analysis_result_filename(argument) bbs_count, bbs_hash, uses_file = self.__parse_raw_output( From 95e0f705d6e44b58a64ffb61b54de5637204019a Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 01:18:29 +0300 Subject: [PATCH 09/13] fix(qbdi): force clean build for the C tracer Added a command to remove CMakeCache.txt before compilation. This ensures that changes to the tracer source or headers are correctly reflected in the compiled library. Signed-off-by: Andrei Carp --- .../arguments_fuzzing/qbdi_analysis.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py index 6e0ed30..46e257c 100644 --- a/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py +++ b/attack_surface_approximation/arguments_fuzzing/qbdi_analysis.py @@ -130,10 +130,14 @@ def __create_container(self) -> None: ) self.__container.exec_run( + "rm -f CMakeCache.txt libqbdi_tracer.so", + workdir=self.__configuration.CONTAINER_SO_FOLDER, + ) + cmake_result = self.__container.exec_run( "cmake .", workdir=self.__configuration.CONTAINER_SO_FOLDER, ) - self.__container.exec_run( + make_result = self.__container.exec_run( "make", workdir=self.__configuration.CONTAINER_SO_FOLDER, ) From 5d62997299212ba97393b1cc17ae928d04f31abe Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 01:20:55 +0300 Subject: [PATCH 10/13] fix(fuzzer): prioritize simple flags in fuzzing sequence Modified the generator to test simple flags before complex combinations. This prevents valid flags from being ignored due to hash collisions with previously seen invalid combinations. Signed-off-by: Andrei Carp --- .../arguments_fuzzing/fuzzing_sequence_generator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/attack_surface_approximation/arguments_fuzzing/fuzzing_sequence_generator.py b/attack_surface_approximation/arguments_fuzzing/fuzzing_sequence_generator.py index 5a5be19..90a1efd 100644 --- a/attack_surface_approximation/arguments_fuzzing/fuzzing_sequence_generator.py +++ b/attack_surface_approximation/arguments_fuzzing/fuzzing_sequence_generator.py @@ -78,14 +78,15 @@ def generate_fuzzing_arguments( ) -> ArgumentsGenerator: arg = FileArgument(self.canary_filename) yield arg - if ArgumentRole.FILE_ENABLER not in arg.get_roles_based_on_analysis( - self.last_analysis_result, bbs_hashes_baseline - ): - for argument in self.arguments: - yield ArgumentPlusFileArgument(argument, self.canary_filename) yield ArgumentArgument("-") for argument in self.arguments: yield ArgumentArgument(argument) yield ArgumentStringArgument(argument, self.canary_string) + + if ArgumentRole.FILE_ENABLER not in arg.get_roles_based_on_analysis( + self.last_analysis_result, bbs_hashes_baseline + ): + for argument in self.arguments: + yield ArgumentPlusFileArgument(argument, self.canary_filename) From 9e210dbc3ff7c8f43ab1b409ee8c08699c6400ab Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 01:44:03 +0300 Subject: [PATCH 11/13] fix(fuzzer): add null hash guards and fix return types Corrected the return type to bool and added safety checks for null instrumentation hashes in both the validation logic and history tracking. This prevents crashes and incorrect deduplication when Docker runs fail. Signed-off-by: Andrei Carp --- .../arguments_fuzzing/fuzzer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/attack_surface_approximation/arguments_fuzzing/fuzzer.py b/attack_surface_approximation/arguments_fuzzing/fuzzer.py index e622fe0..8f9320d 100644 --- a/attack_surface_approximation/arguments_fuzzing/fuzzer.py +++ b/attack_surface_approximation/arguments_fuzzing/fuzzer.py @@ -56,11 +56,15 @@ def __generate_baseline_hashes(self) -> typing.Generator[str, None, None]: for argument in arguments: analysis_result = self.analysis.analyze(argument) - yield analysis_result.bbs_hash + if analysis_result.bbs_hash is not None: + yield analysis_result.bbs_hash def __check_if_argument_is_valid( self, argument: ArgumentsPair, result: QBDIAnalysis - ) -> None: + ) -> bool: + if result.bbs_hash is None: + return False + if ( argument.get_roles_based_on_analysis(result, self.baseline_hashes) and result.bbs_hash not in self.old_hashes # noqa: W503 @@ -91,7 +95,8 @@ def get_valid_argument( # generates a different hash than the baseline ones, it will be detected # as a false flag because of the sequence generation: --flag first, --flag # afterwards. - self.old_hashes.append(result.bbs_hash) + if result.bbs_hash is not None: + self.old_hashes.append(result.bbs_hash) self.arguments_generator.update_last_analysis_result(result) From eac6090d181a9b05b900e477640fff70f5a68055 Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 14:58:53 +0300 Subject: [PATCH 12/13] refactor(cli): implement output buffering for analyze command Decoupled business logic from presentation in cli.py by introducing run_detection and run_fuzzing helpers. Updated the analyze command to collect all results before rendering, ensuring instantaneous output and eliminating visual latency between static and dynamic analysis phases. Signed-off-by: Andrei Carp --- attack_surface_approximation/cli.py | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/attack_surface_approximation/cli.py b/attack_surface_approximation/cli.py index 84939cc..8cc45e7 100644 --- a/attack_surface_approximation/cli.py +++ b/attack_surface_approximation/cli.py @@ -67,6 +67,11 @@ def generate(heuristic: str, output: str, top: int, elf: str = None) -> None: ) +def run_detection(elf: str) -> typing.List[InputStreams]: + detector = InputStreamsDetector(elf) + return detector.detect_all() + + @cli.command( help="Statically detect what input streams are used by an executable." ) @@ -77,13 +82,11 @@ def generate(heuristic: str, output: str, top: int, elf: str = None) -> None: help="ELF Executable", ) def detect(elf: str) -> None: - detector = InputStreamsDetector(elf) - streams = detector.detect_all() - + streams = run_detection(elf) print_detected_streams(streams) -def print_detected_streams(streams: InputStreams) -> None: +def print_detected_streams(streams: typing.List[InputStreams]) -> None: if not any(streams): print_no_detected_stream() else: @@ -94,13 +97,22 @@ def print_no_detected_stream() -> None: print("No input mechanism was detected for the given program.") -def print_multiple_detected_streams(streams: dict) -> None: +def print_multiple_detected_streams(streams: typing.List[InputStreams]) -> None: print("Several input mechanisms were detected for the given program:\n") table = build_detected_streams_table(streams) print(table) +def run_fuzzing(elf: str, dictionary: str) -> typing.List[ArgumentsPair]: + generator = ArgumentsGenerator() + generator.load(dictionary) + possible_arguments = generator.get_arguments() + + fuzzer = ArgumentsFuzzer(elf, possible_arguments) + return fuzzer.get_all_valid_arguments() + + @cli.command(help="Fuzz the arguments of an executable.") @click.option( "--elf", @@ -115,13 +127,7 @@ def print_multiple_detected_streams(streams: dict) -> None: help="Arguments dictionary", ) def fuzz(elf: str, dictionary: str) -> None: - generator = ArgumentsGenerator() - generator.load(dictionary) - possible_arguments = generator.get_arguments() - - fuzzer = ArgumentsFuzzer(elf, possible_arguments) - actual_arguments = fuzzer.get_all_valid_arguments() - + actual_arguments = run_fuzzing(elf, dictionary) print_arguments(actual_arguments) @@ -161,7 +167,7 @@ def build_arguments_table(arguments: typing.List[ArgumentsPair]) -> Table: return table -def build_detected_streams_table(streams: dict) -> Table: +def build_detected_streams_table(streams: typing.List[InputStreams]) -> Table: table = Table() table.add_column("Stream") @@ -187,11 +193,13 @@ def build_detected_streams_table(streams: dict) -> Table: required=True, help="Arguments dictionary", ) -@click.pass_context -def analyze(ctx: click.Context, elf: str, dictionary: str) -> None: - ctx.invoke(detect, elf=elf) +def analyze(elf: str, dictionary: str) -> None: + streams = run_detection(elf) + actual_arguments = run_fuzzing(elf, dictionary) + + print_detected_streams(streams) print("") - ctx.invoke(fuzz, elf=elf, dictionary=dictionary) + print_arguments(actual_arguments) def main() -> None: From 56536b29d3a056c080cf327a2ec2edd77f80cb8e Mon Sep 17 00:00:00 2001 From: Andrei Carp Date: Sun, 10 May 2026 15:11:45 +0300 Subject: [PATCH 13/13] build(deps): stabilize versions and patch security vulnerabilities Pinned all dependencies in pyproject.toml to exact versions to ensure environment reproducibility. Updated the black package to a secure version to resolve two critical security vulnerabilities: arbitrary file write via unsanitized cache filenames and Regular Expression Denial of Service (ReDoS). Fixes: #1, Fixes: #2 Signed-off-by: Andrei Carp --- pyproject.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b35b3e2..ea2c127 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,20 @@ authors = ["OpenCRS"] version = "0.1.0" [tool.poetry.dependencies] -python = "^3.12" -pycparser = "^2.21" -pyelftools = "^0.28" +python = "==3.12.7" +pycparser = "==2.23" +pyelftools = "==0.29" docker = "==7.1.0" -rich = "^12.5.1" -click = "^8.1.3" +rich = "==12.6.0" +click = "==8.3.3" commons = {path = "../commons", develop = false} [tool.poetry.dev-dependencies] -black = "^22.6.0" -isort = "^5.10.1" -pylint = "^2.14.4" -pyproject-flake8 = "^0.0.1-alpha.5" -flake8-annotations = "^2.9.1" +black = "==26.3.1" +isort = "==5.13.2" +pylint = "==2.17.7" +pyproject-flake8 = "==0.0.1a5" +flake8-annotations = "==2.9.1" [tool.poetry.scripts] attack_surface_approximation = "attack_surface_approximation.cli:main"