Skip to content

Commit 8723762

Browse files
sh4dr0xutksh1
andauthored
test: add fuzzer plugin parser and contract coverage (#691)
Co-authored-by: Utkarsh Singh <[email protected]>
1 parent bea9976 commit 8723762

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Contract and parser tests for the fuzzer plugin.
3+
4+
These tests load the real plugins/fuzzer/metadata.json, validate it
5+
through the project PluginMetadataValidator, render commands through the
6+
real PluginManager, and call the real parser.py parse() function.
7+
8+
Related to issue #497: Add parser and contract coverage for plugin `fuzzer`
9+
"""
10+
11+
import asyncio
12+
import json
13+
import sys
14+
from pathlib import Path
15+
16+
import pytest
17+
18+
REPO_ROOT = Path(__file__).resolve().parents[2]
19+
sys.path.insert(0, str(REPO_ROOT))
20+
21+
from backend.secuscan.plugin_validator import PluginMetadataValidator
22+
from backend.secuscan.plugins import PluginManager
23+
from plugins.fuzzer.parser import parse
24+
25+
PLUGIN_DIR = REPO_ROOT / "plugins" / "fuzzer"
26+
PLUGINS_DIR = REPO_ROOT / "plugins"
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# Metadata contract tests
31+
# ---------------------------------------------------------------------------
32+
33+
34+
def test_fuzzer_metadata_file_exists():
35+
"""metadata.json must exist at the expected plugin path."""
36+
assert (PLUGIN_DIR / "metadata.json").exists()
37+
38+
39+
def test_fuzzer_metadata_is_valid_json():
40+
"""metadata.json must be valid, parseable JSON."""
41+
raw = (PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")
42+
data = json.loads(raw)
43+
assert isinstance(data, dict)
44+
45+
46+
def test_fuzzer_passes_validator():
47+
"""The full PluginMetadataValidator must accept the plugin without errors."""
48+
result = PluginMetadataValidator(PLUGIN_DIR).validate()
49+
assert result.valid, "Plugin validation errors:\n" + "\n".join(
50+
e.display() for e in result.errors
51+
)
52+
53+
54+
def test_fuzzer_metadata_id_matches_directory():
55+
"""Plugin id in metadata.json must match the directory name."""
56+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
57+
assert data["id"] == "fuzzer"
58+
59+
60+
def test_fuzzer_engine_is_python3():
61+
"""Engine binary must be 'python3'."""
62+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
63+
assert data["engine"]["type"] == "cli"
64+
assert data["engine"]["binary"] == "python3"
65+
66+
67+
def test_fuzzer_has_required_target_field():
68+
"""Plugin must declare a required 'target' field."""
69+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
70+
fields = {f["id"]: f for f in data["fields"]}
71+
assert "target" in fields, "Missing required field: target"
72+
assert fields["target"]["required"] is True
73+
74+
75+
def test_fuzzer_output_parser_is_custom():
76+
"""Parser type must be 'custom', backed by parser.py."""
77+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
78+
assert data["output"]["parser"] == "custom"
79+
80+
81+
def test_fuzzer_parser_file_exists():
82+
"""parser.py must exist alongside metadata.json."""
83+
assert (PLUGIN_DIR / "parser.py").exists()
84+
85+
86+
def test_fuzzer_requires_consent():
87+
"""Fuzzer is exploit-level and must require consent."""
88+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
89+
assert data["safety"]["requires_consent"] is True
90+
91+
92+
def test_fuzzer_safety_level_is_exploit():
93+
"""Safety level must be 'exploit'."""
94+
data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8"))
95+
assert data["safety"]["level"] == "exploit"
96+
97+
98+
# ---------------------------------------------------------------------------
99+
# Command rendering tests via real PluginManager
100+
# ---------------------------------------------------------------------------
101+
102+
103+
def test_fuzzer_command_renders_with_target(setup_test_environment):
104+
"""PluginManager must produce a valid command for a target."""
105+
manager = PluginManager(str(PLUGINS_DIR))
106+
asyncio.run(manager.load_plugins())
107+
108+
command = manager.build_command("fuzzer", {"target": "https://secuscan.in"})
109+
110+
assert command is not None, "build_command returned None for valid inputs"
111+
assert "python3" in command
112+
assert "https://secuscan.in" in command
113+
114+
115+
def test_fuzzer_command_contains_target_token(setup_test_environment):
116+
"""Rendered command must contain the target value."""
117+
manager = PluginManager(str(PLUGINS_DIR))
118+
asyncio.run(manager.load_plugins())
119+
120+
command = manager.build_command("fuzzer", {"target": "https://example.com"})
121+
assert "https://example.com" in command
122+
123+
124+
def test_fuzzer_drops_target_token_when_absent(setup_test_environment):
125+
"""When 'target' is omitted, no unresolved placeholder must appear."""
126+
manager = PluginManager(str(PLUGINS_DIR))
127+
asyncio.run(manager.load_plugins())
128+
129+
rendered = manager.build_command("fuzzer", {})
130+
assert rendered is not None
131+
assert not any("{" in token for token in rendered), "Unresolved placeholder leaked"
132+
133+
134+
def test_fuzzer_loaded_by_plugin_manager(setup_test_environment):
135+
"""PluginManager must successfully load fuzzer from the real plugins directory."""
136+
manager = PluginManager(str(PLUGINS_DIR))
137+
asyncio.run(manager.load_plugins())
138+
139+
plugin = manager.get_plugin("fuzzer")
140+
assert plugin is not None
141+
assert plugin.id == "fuzzer"
142+
assert plugin.name == "Payload Fuzzer"
143+
144+
145+
# ---------------------------------------------------------------------------
146+
# Parser contract tests against the real parser.py
147+
# ---------------------------------------------------------------------------
148+
149+
_FUZZER_OUTPUT_FIXTURE = (
150+
"Fuzzer simulation\n"
151+
"target=https://secuscan.in\n"
152+
"payload_count=1000\n"
153+
"found injection point at /search\n"
154+
"critical: exploit successful at /admin\n"
155+
)
156+
157+
158+
def test_fuzzer_parser_returns_required_keys():
159+
"""parse() must return a dict with 'findings', 'count', and 'items' keys."""
160+
result = parse(_FUZZER_OUTPUT_FIXTURE)
161+
assert isinstance(result, dict)
162+
assert "findings" in result
163+
assert "count" in result
164+
assert "items" in result
165+
166+
167+
def test_fuzzer_parser_count_matches_findings():
168+
"""'count' must equal len(findings)."""
169+
result = parse(_FUZZER_OUTPUT_FIXTURE)
170+
assert result["count"] == len(result["findings"])
171+
172+
173+
def test_fuzzer_parser_finding_has_required_keys():
174+
"""Each finding must have title, category, severity, description, remediation, metadata."""
175+
result = parse(_FUZZER_OUTPUT_FIXTURE)
176+
assert result["findings"], "Expected at least one finding"
177+
for finding in result["findings"]:
178+
for key in ("title", "category", "severity", "description", "remediation", "metadata"):
179+
assert key in finding, f"Finding missing key: {key}"
180+
181+
182+
def test_fuzzer_parser_severity_classification():
183+
"""Lines with exploit/critical keywords must be 'high'; found/injection 'low'; others 'info'."""
184+
result = parse(_FUZZER_OUTPUT_FIXTURE)
185+
findings = {f["description"]: f["severity"] for f in result["findings"]}
186+
187+
assert findings["Fuzzer simulation"] == "info"
188+
assert findings["target=https://secuscan.in"] == "info"
189+
assert findings["payload_count=1000"] == "info"
190+
assert findings["found injection point at /search"] == "high"
191+
assert findings["critical: exploit successful at /admin"] == "high"
192+
193+
194+
def test_fuzzer_parser_empty_output():
195+
"""Parser must handle empty input and return empty findings without raising."""
196+
result = parse("")
197+
assert result["findings"] == []
198+
assert result["count"] == 0
199+
assert result["items"] == []
200+
201+
202+
def test_fuzzer_parser_high_severity_on_critical():
203+
"""Lines containing 'critical' must produce high severity findings."""
204+
result = parse("critical vulnerability detected\n")
205+
assert result["findings"][0]["severity"] == "high"
206+
207+
208+
def test_fuzzer_parser_low_severity_on_found():
209+
"""Lines containing 'found' must produce low severity findings."""
210+
result = parse("found open endpoint\n")
211+
assert result["findings"][0]["severity"] == "low"
212+
213+
214+
def test_fuzzer_parser_respects_300_line_limit():
215+
"""Parser must cap output at 300 lines."""
216+
big_output = "\n".join(f"line {i}" for i in range(500))
217+
result = parse(big_output)
218+
assert result["count"] <= 300
219+
assert len(result["items"]) <= 300

0 commit comments

Comments
 (0)