From 66211c29e652157df3f9fae690cea15907380ba0 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sat, 3 Jan 2026 00:09:59 +0100 Subject: [PATCH] fix 109 --- aperisolve/analyzers/base_analyzer.py | 1 + aperisolve/analyzers/decomposer.py | 111 +++++++++++++------------- aperisolve/analyzers/image_resize.py | 75 +++++++++-------- aperisolve/analyzers/openstego.py | 1 + aperisolve/workers.py | 20 ++++- 5 files changed, 109 insertions(+), 99 deletions(-) diff --git a/aperisolve/analyzers/base_analyzer.py b/aperisolve/analyzers/base_analyzer.py index 5f20a05..2ead24a 100644 --- a/aperisolve/analyzers/base_analyzer.py +++ b/aperisolve/analyzers/base_analyzer.py @@ -161,6 +161,7 @@ class SubprocessAnalyzer(ABC): self.update_result(result) except Exception as e: self.update_result({"status": "error", "error": str(e)}) + raise def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool: """Check if the result is an error.""" diff --git a/aperisolve/analyzers/decomposer.py b/aperisolve/analyzers/decomposer.py index 9946c6e..a3ff02a 100644 --- a/aperisolve/analyzers/decomposer.py +++ b/aperisolve/analyzers/decomposer.py @@ -20,73 +20,70 @@ class DecomposerAnalyzer(SubprocessAnalyzer): def get_results(self, _: Optional[str] = None) -> dict[str, Any]: """Analyze an image submission using bits decomposition.""" - try: - img = Image.open(self.input_img) - converted = False + img = Image.open(self.input_img) + converted = False - if img.mode == "P": - img = img.convert("RGB") - converted = True + if img.mode == "P": + img = img.convert("RGB") + converted = True - img_np = np.array(img) + img_np = np.array(img) - # Handle grayscale, RGB, RGBA, etc. - if len(img_np.shape) == 2: - channels = 1 - else: - channels = img_np.shape[2] + # Handle grayscale, RGB, RGBA, etc. + if len(img_np.shape) == 2: + channels = 1 + else: + channels = img_np.shape[2] - if channels > 1: - channel_names = ["Red", "Green", "Blue", "Alpha"] - else: - channel_names = ["Grayscale"] + if channels > 1: + channel_names = ["Red", "Green", "Blue", "Alpha"] + else: + channel_names = ["Grayscale"] - image_json = {} + image_json = {} - # Add Superimposed RGB bit planes - if channels >= 3: - superimposed_json = [] - for bit in range(8): - bit_mask = 1 << bit - rgb_planes = [((img_np[..., c] & bit_mask) >> bit) * 255 for c in range(3)] - rgb_image = np.stack(rgb_planes, axis=-1).astype(np.uint8) - rgb_img = Image.fromarray(rgb_image, mode="RGB") - img_name = f"superimposed_bit_{bit}.png" - out_path = self.output_dir / img_name - dl_path = Path(self.output_dir.name) / img_name - superimposed_json.append("/image/" + str(dl_path)) - rgb_img.save(out_path) + # Add Superimposed RGB bit planes + if channels >= 3: + superimposed_json = [] + for bit in range(8): + bit_mask = 1 << bit + rgb_planes = [((img_np[..., c] & bit_mask) >> bit) * 255 for c in range(3)] + rgb_image = np.stack(rgb_planes, axis=-1).astype(np.uint8) + rgb_img = Image.fromarray(rgb_image, mode="RGB") + img_name = f"superimposed_bit_{bit}.png" + out_path = self.output_dir / img_name + dl_path = Path(self.output_dir.name) / img_name + superimposed_json.append("/image/" + str(dl_path)) + rgb_img.save(out_path) - image_json["Superimposed"] = superimposed_json + image_json["Superimposed"] = superimposed_json - # Process individual channels - for c in range(channels): - channel_data = img_np[..., c] if channels > 1 else img_np - channel_label = channel_names[c] - channel_json = [] - for bit in range(8): - bit_mask = 1 << bit - bit_plane = ((channel_data & bit_mask) >> bit) * 255 - bit_img = Image.fromarray(bit_plane.astype(np.uint8), mode="L") - img_name = f"{channel_label}_bit_{bit}.png" - out_path = self.output_dir / img_name - dl_path = Path(self.output_dir.name) / img_name - channel_json.append("/image/" + str(dl_path)) - bit_img.save(out_path) - image_json[channel_label] = channel_json + # Process individual channels + for c in range(channels): + channel_data = img_np[..., c] if channels > 1 else img_np + channel_label = channel_names[c] + channel_json = [] + for bit in range(8): + bit_mask = 1 << bit + bit_plane = ((channel_data & bit_mask) >> bit) * 255 + bit_img = Image.fromarray(bit_plane.astype(np.uint8), mode="L") + img_name = f"{channel_label}_bit_{bit}.png" + out_path = self.output_dir / img_name + dl_path = Path(self.output_dir.name) / img_name + channel_json.append("/image/" + str(dl_path)) + bit_img.save(out_path) + image_json[channel_label] = channel_json - output = { - "status": "ok", - "images": image_json, - } - if converted: - output["note"] = ( - "Image contains a color palette and was converted to RGB for processing." - ) + output = { + "status": "ok", + "images": image_json, + } + if converted: + output["note"] = ( + "Image contains a color palette and was converted to RGB for processing." + ) - return output - except Exception as e: - return {"status": "error", "error": str(e)} + return output def analyze_decomposer(input_img: Path, output_dir: Path) -> None: diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py index 4b4a12a..c853350 100644 --- a/aperisolve/analyzers/image_resize.py +++ b/aperisolve/analyzers/image_resize.py @@ -115,53 +115,50 @@ class ResizeAnalyzer(SubprocessAnalyzer): """ logs = [] - try: - with self.input_img.open("rb") as image: - img_bytes = bytearray(image.read()) - try: - self.png = PNG.from_bytearray(img_bytes) - except ValueError: - return { - "status": "error", - "error": "IHDR chunk is not the first chunk, or PNG has invalid structure.", - } - - # Already correct - if self.png.crc == self.png.compute_crc(): - return { - "status": "ok", - "note": f"PNG is already valid with dimensions " - f"{self.png.width}x{self.png.height} and crc 0x{self.png.crc:08x}.", - } - - saved_img_urls = [] - - # No match found - if not (db_matches := self._lookup_crc()) and not ( - db_matches := self._search_height_crc() - ): + with self.input_img.open("rb") as image: + img_bytes = bytearray(image.read()) + try: + self.png = PNG.from_bytearray(img_bytes) + except ValueError: return { "status": "error", - "error": "Failure: No matching dimensions found.", + "error": "IHDR chunk is not the first chunk, or PNG has invalid structure.", } - # Match found, saving image(s) - logs.append(f"Target CRC found: 0x{self.png.crc:08x}") - self.output_dir.mkdir(parents=True, exist_ok=True) - for png_match in db_matches: - img_name = self._write_recovered_png(img_bytes, png_match) - logs.append(f"Image saved: {img_name}") - # Append the formatted URL to our list - saved_img_urls.append("/image/" + str(Path(self.output_dir.name) / img_name)) - + # Already correct + if self.png.crc == self.png.compute_crc(): return { "status": "ok", - "output": logs, - "png_images": saved_img_urls, + "note": f"PNG is already valid with dimensions " + f"{self.png.width}x{self.png.height} and crc 0x{self.png.crc:08x}.", } - except Exception as e: - return {"status": "error", "error": str(e)} + saved_img_urls = [] + + # No match found + if not (db_matches := self._lookup_crc()) and not ( + db_matches := self._search_height_crc() + ): + return { + "status": "error", + "error": "Failure: No matching dimensions found.", + } + + # Match found, saving image(s) + logs.append(f"Target CRC found: 0x{self.png.crc:08x}") + self.output_dir.mkdir(parents=True, exist_ok=True) + for png_match in db_matches: + img_name = self._write_recovered_png(img_bytes, png_match) + logs.append(f"Image saved: {img_name}") + # Append the formatted URL to our list + saved_img_urls.append("/image/" + str(Path(self.output_dir.name) / img_name)) + + return { + "status": "ok", + "output": logs, + "png_images": saved_img_urls, + } + def analyze_image_resize(input_img: Path, output_dir: Path) -> None: diff --git a/aperisolve/analyzers/openstego.py b/aperisolve/analyzers/openstego.py index 2588150..559482a 100644 --- a/aperisolve/analyzers/openstego.py +++ b/aperisolve/analyzers/openstego.py @@ -44,6 +44,7 @@ class OpenStegoAnalyzer(SubprocessAnalyzer): self.update_result(result) except Exception as e: self.update_result({"status": "error", "error": str(e)}) + raise def process_output(self, stdout: str, stderr: str) -> str | list[str] | dict[str, str]: """Process the stdout/stderr.""" diff --git a/aperisolve/workers.py b/aperisolve/workers.py index 8ab66ef..87a499c 100644 --- a/aperisolve/workers.py +++ b/aperisolve/workers.py @@ -77,12 +77,26 @@ def analyze_image(submission_hash: str) -> None: def run_analyzer(analyzer_func: Any, *args: Any) -> None: """Run an analyzer function in a separate thread.""" + analyzer_name = analyzer_func.__name__.replace("analyze_", "") try: analyzer_func(*args) except Exception as e: - sentry_sdk.capture_exception(e) - print(f"Error in {analyzer_func.__name__}: {e}") - sentry_sdk.capture_exception(e) + print(f"Error in {analyzer_name}: {e}") + with sentry_sdk.push_scope() as scope: + scope.set_tag("analyzer", analyzer_name) + scope.set_tag("submission_hash", submission_hash) + scope.set_context( + "analyzer_info", + { + "tool": analyzer_name, + "image_path": str(img_path), + "result_path": str(result_path), + "filename": submission.filename, + "deep_analysis": submission.deep_analysis, + }, + ) + scope.fingerprint = ["analyzer-error", analyzer_name] + sentry_sdk.capture_exception(e) analyzers = [ (analyze_binwalk, img_path, result_path),