mirror of
https://github.com/416rehman/DeepZero-Agentic-Vulnerability-Research-Pipeline
synced 2026-08-09 11:55:01 +00:00
test coverage: add tests for stages/llm.py and ghidra_decompile tool, fix pe_ingest B110
- test_stages_llm: 13 tests covering process flow, caching, classification, template vars, artifact loading - test_ghidra_decompile: 7 tests covering error paths, successful/failed decompilation, cache skip - fix imphash extraction B110: add debug logging and noqa annotation - total: 129 tests (up from 74)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from deepzero.engine.stage import StageContext, StageSpec
|
||||
from deepzero.engine.state import StageOutput
|
||||
from tools.ghidra_decompile.ghidra_decompile import GhidraDecompile
|
||||
|
||||
|
||||
def _make_ctx(tmp_path, config=None, global_config=None):
|
||||
sample_path = tmp_path / "test.sys"
|
||||
sample_path.write_bytes(b"MZ")
|
||||
|
||||
sample_dir = tmp_path / "samples" / "abc123"
|
||||
sample_dir.mkdir(parents=True)
|
||||
|
||||
history = {"discover": StageOutput(status="completed", data={"sha256": "abc123"})}
|
||||
return StageContext(
|
||||
sample_path=sample_path,
|
||||
sample_dir=sample_dir,
|
||||
history=history,
|
||||
config=config or {},
|
||||
pipeline_dir=tmp_path,
|
||||
global_config=global_config or {},
|
||||
llm=None,
|
||||
)
|
||||
|
||||
|
||||
class TestGhidraDecompileProcess:
|
||||
def _make_tool(self, tmp_path):
|
||||
spec = StageSpec(name="decompile", tool="ghidra_decompile")
|
||||
tool = GhidraDecompile(spec)
|
||||
tool._tool_dir = tmp_path / "tool"
|
||||
tool._tool_dir.mkdir(parents=True, exist_ok=True)
|
||||
return tool
|
||||
|
||||
def test_no_ghidra_dir(self, tmp_path):
|
||||
tool = self._make_tool(tmp_path)
|
||||
ctx = _make_ctx(tmp_path, global_config={"tools": {}})
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "ghidra" in result.error.lower()
|
||||
|
||||
def test_ghidra_not_found(self, tmp_path):
|
||||
tool = self._make_tool(tmp_path)
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
global_config={"tools": {"ghidra": {"install_dir": "/nonexistent/ghidra"}}},
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "not found" in result.error
|
||||
|
||||
def test_no_strategy(self, tmp_path):
|
||||
tool = self._make_tool(tmp_path)
|
||||
ghidra_dir = tmp_path / "ghidra"
|
||||
ghidra_dir.mkdir()
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={},
|
||||
global_config={"tools": {"ghidra": {"install_dir": str(ghidra_dir)}}},
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "strategy" in result.error
|
||||
|
||||
@patch("tools.ghidra_decompile.ghidra_decompile.run_ghidra_headless")
|
||||
def test_successful_decompilation(self, mock_run, tmp_path):
|
||||
tool = self._make_tool(tmp_path)
|
||||
ghidra_dir = tmp_path / "ghidra"
|
||||
ghidra_dir.mkdir()
|
||||
|
||||
scripts_dir = tool._tool_dir / "scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "extract_dispatch.py").write_text("# post-script")
|
||||
|
||||
mock_run.return_value = {
|
||||
"success": True,
|
||||
"device_name": "TestDriver",
|
||||
"function_count": 42,
|
||||
}
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={"strategy": "extract_dispatch.py"},
|
||||
global_config={"tools": {"ghidra": {"install_dir": str(ghidra_dir)}}},
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "completed"
|
||||
assert result.data["device_name"] == "TestDriver"
|
||||
assert result.data["function_count"] == 42
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("tools.ghidra_decompile.ghidra_decompile.run_ghidra_headless")
|
||||
def test_failed_decompilation(self, mock_run, tmp_path):
|
||||
tool = self._make_tool(tmp_path)
|
||||
ghidra_dir = tmp_path / "ghidra"
|
||||
ghidra_dir.mkdir()
|
||||
|
||||
scripts_dir = tool._tool_dir / "scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "extract_dispatch.py").write_text("# post-script")
|
||||
|
||||
mock_run.return_value = {"success": False, "error": "timeout after 300s"}
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={"strategy": "extract_dispatch.py"},
|
||||
global_config={"tools": {"ghidra": {"install_dir": str(ghidra_dir)}}},
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "timeout" in result.error
|
||||
|
||||
|
||||
class TestGhidraDecompileShouldSkip:
|
||||
def test_skips_when_cached(self, tmp_path):
|
||||
spec = StageSpec(name="decompile", tool="ghidra_decompile")
|
||||
tool = GhidraDecompile(spec)
|
||||
ctx = _make_ctx(tmp_path)
|
||||
|
||||
cached = ctx.sample_dir / "decompiled" / "ghidra_result.json"
|
||||
cached.parent.mkdir(parents=True)
|
||||
cached.write_text('{"success": true}')
|
||||
|
||||
reason = tool.should_skip(ctx)
|
||||
assert reason is not None
|
||||
assert "cached" in reason
|
||||
|
||||
def test_no_skip_when_not_cached(self, tmp_path):
|
||||
spec = StageSpec(name="decompile", tool="ghidra_decompile")
|
||||
tool = GhidraDecompile(spec)
|
||||
ctx = _make_ctx(tmp_path)
|
||||
|
||||
reason = tool.should_skip(ctx)
|
||||
assert reason is None
|
||||
@@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from deepzero.engine.stage import StageContext, StageSpec
|
||||
from deepzero.engine.state import StageOutput
|
||||
from deepzero.stages.llm import GenericLLM
|
||||
|
||||
|
||||
def _make_ctx(tmp_path, config=None, llm=None, history_data=None):
|
||||
sample_path = tmp_path / "test.sys"
|
||||
sample_path.write_bytes(b"MZ")
|
||||
|
||||
sample_dir = tmp_path / "samples" / "abc123"
|
||||
sample_dir.mkdir(parents=True)
|
||||
|
||||
discover_data = history_data or {"sha256": "abc123", "filename": "test.sys"}
|
||||
history = {"discover": StageOutput(status="completed", data=discover_data)}
|
||||
|
||||
return StageContext(
|
||||
sample_path=sample_path,
|
||||
sample_dir=sample_dir,
|
||||
history=history,
|
||||
config=config or {},
|
||||
pipeline_dir=tmp_path,
|
||||
global_config={},
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
class TestGenericLLMProcess:
|
||||
def _make_tool(self):
|
||||
spec = StageSpec(name="assess", tool="generic_llm")
|
||||
return GenericLLM(spec)
|
||||
|
||||
def test_no_llm_returns_failed(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
ctx = _make_ctx(tmp_path, config={"prompt": "analyze this"}, llm=None)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "no llm" in result.error
|
||||
|
||||
def test_no_prompt_returns_failed(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
mock_llm = MagicMock()
|
||||
ctx = _make_ctx(tmp_path, config={}, llm=mock_llm)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "failed"
|
||||
assert "prompt" in result.error
|
||||
|
||||
def test_successful_assessment(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.complete.return_value = "this driver is vulnerable"
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={"prompt": "analyze {{sample_name}}", "output_file": "result.md"},
|
||||
llm=mock_llm,
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "completed"
|
||||
assert result.artifacts["llm_output"] == "result.md"
|
||||
|
||||
# verify output was written
|
||||
output = ctx.sample_dir / "result.md"
|
||||
assert output.exists()
|
||||
assert output.read_text() == "this driver is vulnerable"
|
||||
|
||||
def test_cached_output_skips_llm_call(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
mock_llm = MagicMock()
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={"prompt": "analyze", "output_file": "result.md"},
|
||||
llm=mock_llm,
|
||||
)
|
||||
# pre-create the cache
|
||||
(ctx.sample_dir / "result.md").write_text("cached result")
|
||||
|
||||
result = tool.process(ctx)
|
||||
assert result.status == "completed"
|
||||
mock_llm.complete.assert_not_called()
|
||||
|
||||
|
||||
class TestGenericLLMClassify:
|
||||
def _make_tool(self):
|
||||
spec = StageSpec(name="assess", tool="generic_llm")
|
||||
return GenericLLM(spec)
|
||||
|
||||
def test_classify_by_pattern(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.complete.return_value = "[VULNERABLE] buffer overflow found"
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={
|
||||
"prompt": "analyze",
|
||||
"classify_by": r"\[(VULNERABLE|NOT_VULNERABLE)\]",
|
||||
},
|
||||
llm=mock_llm,
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert result.data.get("classification") == "vulnerable"
|
||||
|
||||
def test_no_classification_without_match(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.complete.return_value = "no classification marker here"
|
||||
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={"prompt": "analyze", "classify_by": r"\[EXPLOIT\]"},
|
||||
llm=mock_llm,
|
||||
)
|
||||
result = tool.process(ctx)
|
||||
assert "classification" not in result.data
|
||||
|
||||
|
||||
class TestGenericLLMTemplateVars:
|
||||
def _make_tool(self):
|
||||
spec = StageSpec(name="assess", tool="generic_llm")
|
||||
return GenericLLM(spec)
|
||||
|
||||
def test_builds_template_vars_from_history(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
ctx = _make_ctx(
|
||||
tmp_path,
|
||||
config={},
|
||||
history_data={"sha256": "abc123", "filename": "test.sys", "size_bytes": 1024},
|
||||
)
|
||||
vars = tool._build_template_vars(ctx)
|
||||
assert vars["sample_name"] == "test.sys"
|
||||
assert vars["sha256"] == "abc123"
|
||||
assert vars["size_bytes"] == 1024
|
||||
|
||||
def test_loads_json_artifacts(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
ctx = _make_ctx(tmp_path)
|
||||
|
||||
# create a json artifact in sample_dir
|
||||
artifact = ctx.sample_dir / "analysis_result.json"
|
||||
artifact.write_text(json.dumps({"verdict": "safe"}))
|
||||
|
||||
vars = tool._build_template_vars(ctx)
|
||||
assert vars["analysis_result_json"]["verdict"] == "safe"
|
||||
|
||||
def test_loads_text_artifacts(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
ctx = _make_ctx(tmp_path)
|
||||
|
||||
# create a text artifact in sample_dir
|
||||
artifact = ctx.sample_dir / "decompiled.c"
|
||||
artifact.write_text("int main() { return 0; }")
|
||||
|
||||
vars = tool._build_template_vars(ctx)
|
||||
assert "int main()" in vars["decompiled_c"]
|
||||
|
||||
def test_truncates_large_artifacts(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
ctx = _make_ctx(tmp_path, config={"max_context_tokens": 10})
|
||||
|
||||
artifact = ctx.sample_dir / "large.txt"
|
||||
artifact.write_text("x" * 1000)
|
||||
|
||||
vars = tool._build_template_vars(ctx)
|
||||
assert len(vars["large_txt"]) < 1000
|
||||
assert "truncated" in vars["large_txt"]
|
||||
|
||||
|
||||
class TestGenericLLMResolveTemplate:
|
||||
def _make_tool(self):
|
||||
spec = StageSpec(name="assess", tool="generic_llm")
|
||||
return GenericLLM(spec)
|
||||
|
||||
def test_returns_none_for_plain_string(self):
|
||||
tool = self._make_tool()
|
||||
result = tool._resolve_template("just a prompt string")
|
||||
assert result is None
|
||||
|
||||
def test_resolves_absolute_path(self, tmp_path):
|
||||
tool = self._make_tool()
|
||||
f = tmp_path / "prompt.j2"
|
||||
f.write_text("template content")
|
||||
result = tool._resolve_template(str(f))
|
||||
assert result == f
|
||||
|
||||
def test_returns_none_for_missing_absolute(self):
|
||||
tool = self._make_tool()
|
||||
result = tool._resolve_template("/nonexistent/path/prompt.j2")
|
||||
assert result is None
|
||||
@@ -226,7 +226,8 @@ class PEIngest(IngestTool):
|
||||
|
||||
try:
|
||||
meta["imphash"] = pe.get_imphash() or ""
|
||||
except Exception:
|
||||
except (AttributeError, ValueError, Exception) as exc: # noqa: B110 — pefile imphash can fail on malformed imports
|
||||
self.log.debug("imphash extraction failed: %s", exc)
|
||||
meta["imphash"] = ""
|
||||
|
||||
pe.close()
|
||||
|
||||
Reference in New Issue
Block a user