Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions sample/tests/test_apk_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,21 @@ def test_no_manifest_produced_raises(self):
with self.assertRaises(ApktoolRunError):
run_apktool_decode(apk, tdp / "out", ApktoolConfig(apktool_path=str(stub)))

def test_launch_os_error_is_wrapped(self):
with tempfile.TemporaryDirectory() as td:
tdp = Path(td)
apk = tdp / "fake.apk"
apk.write_bytes(b"PK")
with mock.patch(
"venomhook.apk_decoder.subprocess.run",
side_effect=PermissionError("permission denied"),
):
with self.assertRaises(ApktoolRunError) as ctx:
run_apktool_decode(
apk, tdp / "out", ApktoolConfig(apktool_path="/bad/apktool")
)
self.assertIn("could not exec apktool binary", str(ctx.exception))

def test_default_command_includes_force_and_o(self):
with tempfile.TemporaryDirectory() as td:
tdp = Path(td)
Expand Down
15 changes: 15 additions & 0 deletions sample/tests/test_jadx_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,21 @@ def test_failed_invocation_with_no_output_raises(self):
run_jadx(apk, tdp / "out", config=cfg)
self.assertIn("exit=5", str(ctx.exception))

def test_launch_os_error_is_wrapped(self):
with tempfile.TemporaryDirectory() as td:
tdp = Path(td)
apk = tdp / "fake.apk"
apk.write_bytes(b"PK")
with mock.patch(
"venomhook.jadx_runner.subprocess.run",
side_effect=PermissionError("permission denied"),
):
with self.assertRaises(JadxRunError) as ctx:
run_jadx(
apk, tdp / "out", config=JadxConfig(jadx_path="/bad/jadx")
)
self.assertIn("could not exec jadx binary", str(ctx.exception))

def test_partial_failure_with_output_is_accepted(self):
# jadx returns non-zero on partial decompile; if .java exists, succeed.
with tempfile.TemporaryDirectory() as td:
Expand Down
10 changes: 10 additions & 0 deletions sample/tests/test_orchestrator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import unittest
from pathlib import Path
from unittest import mock

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "src"))
Expand All @@ -24,6 +25,15 @@ def test_build_frida_command_attach_and_dry_run(self) -> None:
cmd_str = run_frida("1234", Path("venomhook.js"), frida_path="frida", attach=True, dry_run=True)
self.assertIn("-p 1234", cmd_str)

def test_run_frida_wraps_launch_os_error(self) -> None:
with mock.patch(
"venomhook.orchestrator.subprocess.run",
side_effect=PermissionError("permission denied"),
):
with self.assertRaises(RuntimeError) as ctx:
run_frida("1234", Path("venomhook.js"), frida_path="/bad/frida", attach=True)
self.assertIn("could not exec frida binary", str(ctx.exception))


if __name__ == "__main__":
unittest.main()
12 changes: 12 additions & 0 deletions sample/tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import tempfile
import unittest
from pathlib import Path
from unittest import mock

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
Expand Down Expand Up @@ -119,6 +120,17 @@ def test_ghidra_runner_stub(self) -> None:
runner.run(SAMPLE_STATIC_META, out_static)
self.assertTrue(out_static.exists())

def test_ghidra_runner_wraps_launch_os_error(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
runner = GhidraRunner(headless_cmd=["/bad/analyzeHeadless"], post_script=None)
with mock.patch(
"venomhook.ghidra_runner.subprocess.run",
side_effect=PermissionError("permission denied"),
):
with self.assertRaises(RuntimeError) as ctx:
runner.run(SAMPLE_STATIC_META, Path(tmpdir) / "out.json")
self.assertIn("could not exec Ghidra headless command", str(ctx.exception))


if __name__ == "__main__":
unittest.main()
4 changes: 4 additions & 0 deletions src/venomhook/apk_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ def run_apktool_decode(
raise ApktoolNotFoundError(
f"could not exec apktool binary at {binary!r}: {e}"
) from e
except OSError as e:
raise ApktoolRunError(
f"could not exec apktool binary at {binary!r}: {e}"
) from e

manifest = out / "AndroidManifest.xml"
smali_present = any(out.glob("smali*"))
Expand Down
13 changes: 9 additions & 4 deletions src/venomhook/ghidra_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ def run(self, binary_path: Path, out_static_meta: Path) -> None:
logger.info("running Ghidra headless: %s", " ".join(cmd))
# Pin UTF-8 so Ghidra's i18n stdout/stderr doesn't crash decoding
# under Windows cp949/cp1252 or non-UTF-8 POSIX locales.
result = subprocess.run(
cmd, capture_output=True, text=True,
encoding="utf-8", errors="replace",
)
try:
result = subprocess.run(
cmd, capture_output=True, text=True,
encoding="utf-8", errors="replace",
)
except OSError as e:
raise RuntimeError(
f"could not exec Ghidra headless command at {cmd[0]!r}: {e}"
) from e
if result.returncode != 0:
logger.error("Ghidra headless failed: %s", result.stderr)
raise RuntimeError(f"Ghidra headless failed (code {result.returncode})")
Expand Down
4 changes: 4 additions & 0 deletions src/venomhook/jadx_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ def run_jadx(
raise JadxNotFoundError(
f"could not exec jadx binary at {binary!r}: {e}"
) from e
except OSError as e:
raise JadxRunError(
f"could not exec jadx binary at {binary!r}: {e}"
) from e

java_files = sum(1 for _ in out.rglob("*.java"))
stdout_tail = (completed.stdout or "")[-4096:]
Expand Down
11 changes: 7 additions & 4 deletions src/venomhook/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ def run_frida(
# Pin UTF-8 for both decoding of frida's child output and for writing
# the captured log: the target process can emit non-ASCII strings, and
# the system default codec is cp949/cp1252 on Korean/Western Windows.
proc = subprocess.run(
cmd, stdout=stdout_pipe, stderr=stderr_pipe, text=True,
encoding="utf-8", errors="replace",
)
try:
proc = subprocess.run(
cmd, stdout=stdout_pipe, stderr=stderr_pipe, text=True,
encoding="utf-8", errors="replace",
)
except OSError as e:
raise RuntimeError(f"could not exec frida binary at {frida_path!r}: {e}") from e
if log_file and proc.stdout:
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text(proc.stdout, encoding="utf-8")
Expand Down
Loading