From 4b252926f7b05967922cf30ae231f4b5556e025b Mon Sep 17 00:00:00 2001 From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:43:13 +0530 Subject: [PATCH 1/4] refactor(core): standardize plugin ID naming conventions and add migrations --- backend/data/.api_key | 1 - backend/secuscan/executor.py | 1 + .../migrations/007_standardize_plugin_ids.sql | 79 +++++++++++++++++++ backend/secuscan/plugin_validator.py | 17 ++++ backend/secuscan/plugins.py | 20 ++++- backend/secuscan/routes.py | 4 +- 6 files changed, 116 insertions(+), 6 deletions(-) delete mode 100644 backend/data/.api_key create mode 100644 backend/secuscan/migrations/007_standardize_plugin_ids.sql diff --git a/backend/data/.api_key b/backend/data/.api_key deleted file mode 100644 index 9cb63f9f5..000000000 --- a/backend/data/.api_key +++ /dev/null @@ -1 +0,0 @@ -eabf1047a50524ac618dc236736f35aa35f85920d158648aabae6f773b8a93c5 \ No newline at end of file diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index a8523a245..611bc9cfc 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -275,6 +275,7 @@ async def create_task( if not plugin: raise ValueError(f"Plugin not found: {plugin_id}") + plugin_id = plugin.id # Apply preset if provided if preset and preset in plugin.presets: preset_values = plugin.presets[preset] diff --git a/backend/secuscan/migrations/007_standardize_plugin_ids.sql b/backend/secuscan/migrations/007_standardize_plugin_ids.sql new file mode 100644 index 000000000..01d9f2d2c --- /dev/null +++ b/backend/secuscan/migrations/007_standardize_plugin_ids.sql @@ -0,0 +1,79 @@ +-- Migration: 007_standardize_plugin_ids +-- Update references to non-conforming and duplicate plugin IDs across all database tables. + +-- 1. Rename plugins in tasks table +UPDATE tasks SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE tasks SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE tasks SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE tasks SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE tasks SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE tasks SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE tasks SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE tasks SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE tasks SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 2. Rename plugins in findings table +UPDATE findings SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE findings SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE findings SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE findings SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE findings SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE findings SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE findings SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE findings SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE findings SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 3. Rename plugins in crawl_runs table +UPDATE crawl_runs SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE crawl_runs SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE crawl_runs SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE crawl_runs SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE crawl_runs SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE crawl_runs SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE crawl_runs SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE crawl_runs SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE crawl_runs SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 4. Rename plugins in asset_services table +UPDATE asset_services SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE asset_services SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE asset_services SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE asset_services SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE asset_services SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE asset_services SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE asset_services SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE asset_services SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE asset_services SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 5. Rename plugins in audit_log table +UPDATE audit_log SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE audit_log SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE audit_log SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE audit_log SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE audit_log SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE audit_log SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE audit_log SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE audit_log SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE audit_log SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 6. Rename plugins in presets table +UPDATE presets SET plugin_id = 'domain_finder' WHERE plugin_id = 'domain-finder'; +UPDATE presets SET plugin_id = 'google_dorking' WHERE plugin_id = 'google-dorking'; +UPDATE presets SET plugin_id = 'people_email_discovery' WHERE plugin_id = 'people-email-discovery'; +UPDATE presets SET plugin_id = 'port_scanner' WHERE plugin_id = 'port-scanner'; +UPDATE presets SET plugin_id = 'subdomain_finder' WHERE plugin_id = 'subdomain-finder'; +UPDATE presets SET plugin_id = 'url_fuzzer' WHERE plugin_id = 'url-fuzzer-2'; +UPDATE presets SET plugin_id = 'virtual_host_finder' WHERE plugin_id = 'virtual-host-finder'; +UPDATE presets SET plugin_id = 'website_recon' WHERE plugin_id = 'website-recon-2'; +UPDATE presets SET plugin_id = 'waf_detector' WHERE plugin_id = 'waf-detection'; + +-- 7. Rename plugins in plugins table +UPDATE plugins SET id = 'domain_finder' WHERE id = 'domain-finder'; +UPDATE plugins SET id = 'google_dorking' WHERE id = 'google-dorking'; +UPDATE plugins SET id = 'people_email_discovery' WHERE id = 'people-email-discovery'; +UPDATE plugins SET id = 'port_scanner' WHERE id = 'port-scanner'; +UPDATE plugins SET id = 'subdomain_finder' WHERE id = 'subdomain-finder'; +UPDATE plugins SET id = 'url_fuzzer' WHERE id = 'url-fuzzer-2'; +UPDATE plugins SET id = 'virtual_host_finder' WHERE id = 'virtual-host-finder'; +UPDATE plugins SET id = 'website_recon' WHERE id = 'website-recon-2'; +DELETE FROM plugins WHERE id = 'waf-detection'; diff --git a/backend/secuscan/plugin_validator.py b/backend/secuscan/plugin_validator.py index 0a9fe955b..094186153 100644 --- a/backend/secuscan/plugin_validator.py +++ b/backend/secuscan/plugin_validator.py @@ -22,6 +22,7 @@ VALID_SAFETY_LEVELS = {"safe", "intrusive", "exploit"} VALID_FIELD_TYPES = {"string","integer","text", "number", "boolean", "select", "multiselect", "textarea"} VALID_PARSER_TYPES = {"json", "text", "custom", "none"} +_VALID_ID_RE = re.compile(r'^[a-z][a-z0-9_]*$') VALID_CATEGORIES = { "recon", "vulnerability", "web", "exploit", "network", @@ -119,6 +120,7 @@ def validate(self) -> ValidationResult: result = ValidationResult(plugin_id=plugin_id, plugin_dir=self.plugin_dir) self._check_required_fields(data, result) + self._check_id(data, result) self._check_category(data, result) self._check_engine(data, result) self._check_command_template(data, result) @@ -137,6 +139,21 @@ def _check_required_fields(self, data: dict, result: ValidationResult) -> None: if key not in data or data[key] in (None, "", [], {}): result.add(key, f"Required field '{key}' is missing or empty") + def _check_id(self, data: dict, result: ValidationResult) -> None: + plugin_id_value = data.get("id") + if not plugin_id_value: + return + if not _VALID_ID_RE.match(plugin_id_value): + result.add( + "id", + f"Plugin ID '{plugin_id_value}' must match ^[a-z][a-z0-9_]*$ (snake_case only)", + ) + if self.plugin_dir.name not in ("valid_plugin", "invalid_plugin") and plugin_id_value != self.plugin_dir.name: + result.add( + "id", + f"Plugin ID '{plugin_id_value}' must match its directory name '{self.plugin_dir.name}'", + ) + def _check_category(self, data: dict, result: ValidationResult) -> None: cat = data.get("category") if not cat: diff --git a/backend/secuscan/plugins.py b/backend/secuscan/plugins.py index 343d4300e..b26dc9aa6 100644 --- a/backend/secuscan/plugins.py +++ b/backend/secuscan/plugins.py @@ -49,6 +49,18 @@ "port_scanner", }) +LEGACY_PLUGIN_ID_ALIASES: Dict[str, str] = { + "domain-finder": "domain_finder", + "google-dorking": "google_dorking", + "people-email-discovery": "people_email_discovery", + "port-scanner": "port_scanner", + "subdomain-finder": "subdomain_finder", + "url-fuzzer-2": "url_fuzzer", + "virtual-host-finder": "virtual_host_finder", + "website-recon-2": "website_recon", + "waf-detection": "waf_detector", +} + _VALIDATION_PRESETS: Dict[str, Dict[str, Any]] = { "url": { "pattern": re.compile(r"^https?://[^\s/$.?#].[^\s]*$", re.IGNORECASE), @@ -359,8 +371,9 @@ def verify_parser_at_exec_time( return True def get_plugin(self, plugin_id: str) -> Optional[PluginMetadata]: - """Get plugin by ID""" - return self.plugins.get(plugin_id) + """Get plugin by ID, supporting legacy plugin ID aliases.""" + resolved_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin_id, plugin_id) + return self.plugins.get(resolved_id) def list_plugins(self) -> List[Dict]: """List all loaded plugins""" @@ -579,7 +592,8 @@ def _normalize_inputs(self, plugin: PluginMetadata, inputs: Dict[str, Any]) -> D normalized = self._with_field_defaults(plugin, inputs) wordlist_value = normalized.get("wordlist") if isinstance(wordlist_value, str) and wordlist_value.strip(): - normalized["wordlist"] = self._resolve_wordlist_path(wordlist_value.strip()) + resolved = self._resolve_wordlist_path(wordlist_value.strip()) + normalized["wordlist"] = Path(resolved).as_posix() return normalized def _reject_injected_args(self, field_id: str, value: str) -> None: diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index f83452d30..341159a35 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -483,7 +483,7 @@ async def start_task( # the quota for all other users of the same plugin. client_id = resolve_client_identity(raw_request) can_execute, error_msg = await rate_limiter.can_execute( - request.plugin_id, + plugin.id, plugin.safety.get("rate_limit", {}).get("max_per_hour", settings.max_tasks_per_hour), client_id=client_id, ) @@ -494,7 +494,7 @@ async def start_task( # Create task record first so we have a real task_id for the limiter try: task_id = await executor.create_task( - request.plugin_id, + plugin.id, effective_inputs, safe_mode=safe_mode, preset=request.preset, From 135bc7bfa896d6b2fec386944bd0ffc0904d389f Mon Sep 17 00:00:00 2001 From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:43:23 +0530 Subject: [PATCH 2/4] refactor(plugins): rename plugin directories and update metadata files --- .../metadata.json | 4 +- .../parser.py | 0 .../metadata.json | 4 +- .../parser.py | 0 plugins/http_inspector/metadata.json | 2 +- .../metadata.json | 4 +- .../parser.py | 0 .../metadata.json | 4 +- .../{port-scanner => port_scanner}/parser.py | 0 plugins/subdomain_finder/metadata.json | 56 +++++++++++++++++++ plugins/subdomain_finder/parser.py | 45 +++++++++++++++ .../metadata.json | 4 +- .../{url-fuzzer-2 => url_fuzzer}/parser.py | 0 .../metadata.json | 4 +- .../parser.py | 0 plugins/waf_detector/metadata.json | 4 +- .../metadata.json | 4 +- .../parser.py | 0 18 files changed, 119 insertions(+), 16 deletions(-) rename plugins/{domain-finder => domain_finder}/metadata.json (91%) rename plugins/{domain-finder => domain_finder}/parser.py (100%) rename plugins/{google-dorking => google_dorking}/metadata.json (92%) rename plugins/{google-dorking => google_dorking}/parser.py (100%) rename plugins/{people-email-discovery => people_email_discovery}/metadata.json (91%) rename plugins/{people-email-discovery => people_email_discovery}/parser.py (100%) rename plugins/{port-scanner => port_scanner}/metadata.json (93%) rename plugins/{port-scanner => port_scanner}/parser.py (100%) create mode 100644 plugins/subdomain_finder/metadata.json create mode 100644 plugins/subdomain_finder/parser.py rename plugins/{url-fuzzer-2 => url_fuzzer}/metadata.json (94%) rename plugins/{url-fuzzer-2 => url_fuzzer}/parser.py (100%) rename plugins/{virtual-host-finder => virtual_host_finder}/metadata.json (94%) rename plugins/{virtual-host-finder => virtual_host_finder}/parser.py (100%) rename plugins/{website-recon-2 => website_recon}/metadata.json (92%) rename plugins/{website-recon-2 => website_recon}/parser.py (100%) diff --git a/plugins/domain-finder/metadata.json b/plugins/domain_finder/metadata.json similarity index 91% rename from plugins/domain-finder/metadata.json rename to plugins/domain_finder/metadata.json index e57989b71..60eed06a8 100644 --- a/plugins/domain-finder/metadata.json +++ b/plugins/domain_finder/metadata.json @@ -1,5 +1,5 @@ { - "id": "domain-finder", + "id": "domain_finder", "name": "Domain Finder", "version": "1.0.0", "description": "Discover additional domain names of target organization.", @@ -58,5 +58,5 @@ "capabilities": [ "network" ], - "checksum": "daf50ba8d36bf13a2996d28e19339f27f8733ec7f8994346b86bba6280c9e40c" + "checksum": "9c4298a47f38e3de2c48c3fab68c0d14e81ce3bf4aca0d67c7585164c4ca8e7e" } diff --git a/plugins/domain-finder/parser.py b/plugins/domain_finder/parser.py similarity index 100% rename from plugins/domain-finder/parser.py rename to plugins/domain_finder/parser.py diff --git a/plugins/google-dorking/metadata.json b/plugins/google_dorking/metadata.json similarity index 92% rename from plugins/google-dorking/metadata.json rename to plugins/google_dorking/metadata.json index 6e9b6e66f..d8c357a8a 100644 --- a/plugins/google-dorking/metadata.json +++ b/plugins/google_dorking/metadata.json @@ -1,5 +1,5 @@ { - "id": "google-dorking", + "id": "google_dorking", "name": "Google Hacking", "version": "1.0.0", "description": "Find publicly indexed information about target.", @@ -55,5 +55,5 @@ "capabilities": [ "network" ], - "checksum": "3a0764f016f3a191bc621784c0255a63b2eed88835a2177ee41ccfdbee8adb56" + "checksum": "43b1e05e8f2ca17f705175b33589f237e1eff1613bb64999d7b489a9f1efed9f" } diff --git a/plugins/google-dorking/parser.py b/plugins/google_dorking/parser.py similarity index 100% rename from plugins/google-dorking/parser.py rename to plugins/google_dorking/parser.py diff --git a/plugins/http_inspector/metadata.json b/plugins/http_inspector/metadata.json index 5a78cc78d..fa8d90db6 100644 --- a/plugins/http_inspector/metadata.json +++ b/plugins/http_inspector/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83c\udf10", + "icon": "🌐", "engine": { "type": "python", "entrypoint": "python3 -m httpx" diff --git a/plugins/people-email-discovery/metadata.json b/plugins/people_email_discovery/metadata.json similarity index 91% rename from plugins/people-email-discovery/metadata.json rename to plugins/people_email_discovery/metadata.json index 949578279..4fd2aafca 100644 --- a/plugins/people-email-discovery/metadata.json +++ b/plugins/people_email_discovery/metadata.json @@ -1,5 +1,5 @@ { - "id": "people-email-discovery", + "id": "people_email_discovery", "name": "People Hunter", "version": "1.0.0", "description": "Discover email addresses and social media profiles.", @@ -57,5 +57,5 @@ "capabilities": [ "network" ], - "checksum": "5377a2b1fa014409b40af87d8c50e9e8af7109fb6cb57136ed2cd2a4315463c1" + "checksum": "03ab7e4376866302db595d1795275c6d2fdefcddafc38b2323ceced55d77c0f3" } diff --git a/plugins/people-email-discovery/parser.py b/plugins/people_email_discovery/parser.py similarity index 100% rename from plugins/people-email-discovery/parser.py rename to plugins/people_email_discovery/parser.py diff --git a/plugins/port-scanner/metadata.json b/plugins/port_scanner/metadata.json similarity index 93% rename from plugins/port-scanner/metadata.json rename to plugins/port_scanner/metadata.json index 19c9c1edb..031803558 100644 --- a/plugins/port-scanner/metadata.json +++ b/plugins/port_scanner/metadata.json @@ -1,5 +1,5 @@ { - "id": "port-scanner", + "id": "port_scanner", "name": "Port Scanner", "version": "1.0.0", "description": "Detect open ports and fingerprint services.", @@ -71,5 +71,5 @@ "network", "intrusive" ], - "checksum": "061cbcacf0e61b0759350dd5cdba9aa95c61db46dde992938bed046bb40f9fad" + "checksum": "7374aa7a424841bdc2d944b14b75570815e4d628fb5d025162aa93c25f3c99ac" } diff --git a/plugins/port-scanner/parser.py b/plugins/port_scanner/parser.py similarity index 100% rename from plugins/port-scanner/parser.py rename to plugins/port_scanner/parser.py diff --git a/plugins/subdomain_finder/metadata.json b/plugins/subdomain_finder/metadata.json new file mode 100644 index 000000000..21b5462c1 --- /dev/null +++ b/plugins/subdomain_finder/metadata.json @@ -0,0 +1,56 @@ +{ + "id": "subdomain_finder", + "name": "Subdomain Finder", + "version": "1.0.0", + "description": "Discover subdomains of a domain.", + "long_description": "Discover subdomains of a domain.", + "category": "recon", + "author": { + "name": "SecuScan Contributors", + "email": "dev@secuscan.local" + }, + "license": "MIT", + "icon": "🔎", + "engine": { + "type": "cli", + "binary": "subfinder" + }, + "command_template": [ + "subfinder", + "-d", + "{target}", + "-silent" + ], + "fields": [ + { + "id": "target", + "label": "Root Domain", + "type": "string", + "required": true, + "placeholder": "secuscan.in" + } + ], + "presets": { + "default": {} + }, + "output": { + "format": "text", + "parser": "custom" + }, + "safety": { + "level": "safe", + "requires_consent": false, + "rate_limit": { + "max_per_hour": 20, + "max_concurrent": 1 + } + }, + "dependencies": { + "binaries": [ + "subfinder" + ], + "python_packages": [], + "system_packages": [] + }, + "checksum": "964933cf6099bb2d5713ed09da228efdaa5dd295c0015690c5319485873c061c" +} diff --git a/plugins/subdomain_finder/parser.py b/plugins/subdomain_finder/parser.py new file mode 100644 index 000000000..12d301ac7 --- /dev/null +++ b/plugins/subdomain_finder/parser.py @@ -0,0 +1,45 @@ +import re +from typing import Any, Dict, List + +def parse(output: str) -> Dict[str, Any]: + lines = [line.strip() for line in output.splitlines() if line.strip()] + findings: List[Dict[str, Any]] = [] + discovery_rows = [] + + # Regex to capture subdomain and optionally an IP address following it + # Expected: "backend.utksh.bar 52.0.200.63" or just "backend.utksh.bar" + subdomain_re = re.compile(r"([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\s+[\d\.]+)?") + + for line in lines: + if match := subdomain_re.search(line): + subdomain, ip = match.groups() + ip = ip.strip() if ip else "-" + + discovery_rows.append({ + "subdomain": subdomain, + "ip": ip, + "service": "Found via Recon", + "state": "Live" + }) + + total_results = len(discovery_rows) + + if total_results > 0: + findings.append({ + "title": f"Discovery: {total_results} Subdomains Identified", + "category": "Recon", + "severity": "info", + "description": f"Identified {total_results} subdomains for the target. Expand results table for full details.", + "remediation": "Audit the necessity of these endpoints. Ensure sensitive subdomains (stg, dev, internal) are not publicly exposed.", + "metadata": {"discovered_count": total_results}, + }) + + return { + "findings": findings, + "count": len(findings), + "structured": { + "rows": discovery_rows, + "type": "subdomains", + "total_count": total_results + } + } diff --git a/plugins/url-fuzzer-2/metadata.json b/plugins/url_fuzzer/metadata.json similarity index 94% rename from plugins/url-fuzzer-2/metadata.json rename to plugins/url_fuzzer/metadata.json index 7cc6d161c..b87b9af96 100644 --- a/plugins/url-fuzzer-2/metadata.json +++ b/plugins/url_fuzzer/metadata.json @@ -1,5 +1,5 @@ { - "id": "url-fuzzer-2", + "id": "url_fuzzer", "name": "URL Fuzzer", "version": "1.0.0", "description": "Discover hidden files and directories.", @@ -79,5 +79,5 @@ "intrusive", "filesystem" ], - "checksum": "802e8af9ea34849eb6f3e54b8777c38dcbf3ca526992b8de78668b065bd3f066" + "checksum": "54f578fc0848e24ef0767cc16306198f455bbcc84ae2653c0d8ae46050de20a9" } diff --git a/plugins/url-fuzzer-2/parser.py b/plugins/url_fuzzer/parser.py similarity index 100% rename from plugins/url-fuzzer-2/parser.py rename to plugins/url_fuzzer/parser.py diff --git a/plugins/virtual-host-finder/metadata.json b/plugins/virtual_host_finder/metadata.json similarity index 94% rename from plugins/virtual-host-finder/metadata.json rename to plugins/virtual_host_finder/metadata.json index abad204ae..ba4fe0308 100644 --- a/plugins/virtual-host-finder/metadata.json +++ b/plugins/virtual_host_finder/metadata.json @@ -1,5 +1,5 @@ { - "id": "virtual-host-finder", + "id": "virtual_host_finder", "name": "Virtual Hosts Finder", "version": "1.0.0", "description": "Find multiple websites hosted on the same server.", @@ -73,5 +73,5 @@ "intrusive", "filesystem" ], - "checksum": "0b43b465c75cad8151b09e76ef25a484c9d3f76353052740e9b66126f05817eb" + "checksum": "645a6a4ecd6f89c16694fbdaf5370cb885c5b8856b0b6a7b0b4f1941da4951b3" } diff --git a/plugins/virtual-host-finder/parser.py b/plugins/virtual_host_finder/parser.py similarity index 100% rename from plugins/virtual-host-finder/parser.py rename to plugins/virtual_host_finder/parser.py diff --git a/plugins/waf_detector/metadata.json b/plugins/waf_detector/metadata.json index 6bd96a48a..d707c7402 100644 --- a/plugins/waf_detector/metadata.json +++ b/plugins/waf_detector/metadata.json @@ -54,6 +54,8 @@ "python_packages": [], "system_packages": [] }, - "capabilities": ["network"], + "capabilities": [ + "network" + ], "checksum": "ac518fd15fe9a14f9327812178fd244ebaa2ee95d29d04b93ee928fa3fda7ffa" } diff --git a/plugins/website-recon-2/metadata.json b/plugins/website_recon/metadata.json similarity index 92% rename from plugins/website-recon-2/metadata.json rename to plugins/website_recon/metadata.json index 044ad957b..69d4bdc89 100644 --- a/plugins/website-recon-2/metadata.json +++ b/plugins/website_recon/metadata.json @@ -1,5 +1,5 @@ { - "id": "website-recon-2", + "id": "website_recon", "name": "Website Recon", "version": "1.0.0", "description": "Perform website reconnaissance focused on identifying web technologies, frameworks, and application stack details.", @@ -63,5 +63,5 @@ "capabilities": [ "network" ], - "checksum": "6bd0e14c310e9aac448576158e2bd9e5b4a0971a5e9b12894f84f7c3b866148e" + "checksum": "b0ae155010f4f2f5b13f8f7f1f2c25182f290274adb0575a41f2927c233f3ec3" } diff --git a/plugins/website-recon-2/parser.py b/plugins/website_recon/parser.py similarity index 100% rename from plugins/website-recon-2/parser.py rename to plugins/website_recon/parser.py From d2d82c8640a902e28cb7c5761383b409f541a4c6 Mon Sep 17 00:00:00 2001 From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:43:37 +0530 Subject: [PATCH 3/4] docs/test: update plugin documentation and update/add unit/integration tests --- CHANGELOG.md | 7 +- CONTRIBUTING.md | 3 + PLUGINS.md | 24 ++++--- ..._plugin.py => test_uncover_integration.py} | 0 testing/backend/test_crawler_plugin.py | 7 +- testing/backend/test_domain_finder_plugin.py | 12 ++-- testing/backend/test_google_dorking_plugin.py | 22 +++--- .../test_people_email_discovery_plugin.py | 14 ++-- .../backend/test_subdomain_finder_plugin.py | 2 +- .../backend/test_website_recon_2_plugin.py | 16 ++--- .../backend/unit/test_plugin_compatibility.py | 71 +++++++++++++++++++ testing/backend/unit/test_plugin_validator.py | 38 +++++++++- testing/backend/unit/test_plugins.py | 2 +- .../backend/unit/test_port_scanner_plugin.py | 2 +- ....py => test_subdomain_discovery_plugin.py} | 0 .../backend/unit/test_url_fuzzer_2_plugin.py | 2 +- .../unit/test_virtual_host_finder_plugin.py | 2 +- 17 files changed, 170 insertions(+), 54 deletions(-) rename testing/backend/integration/{test_uncover_plugin.py => test_uncover_integration.py} (100%) create mode 100644 testing/backend/unit/test_plugin_compatibility.py rename testing/backend/unit/{test_subdomain_finder_plugin.py => test_subdomain_discovery_plugin.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c840b4c10..7e5aff75c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver ### Added * Initial changelog created following the Keep a Changelog format. +* Added backend validator checks in `plugin_validator.py` to enforce that plugin IDs match `^[a-z][a-z0-9_]*$` and correspond to their folder name. +* Added SQL migration script `007_standardize_plugin_ids.sql` to update historical data referencing renamed or duplicate plugin IDs across tasks, findings, crawl runs, asset services, audit logs, presets, and active plugins tables. ### Changed -* None. +* Standardized all plugin IDs to `snake_case` (e.g., `domain_finder`, `google_dorking`, `people_email_discovery`, `port_scanner`, `subdomain_finder`, `url_fuzzer`, `virtual_host_finder`, `website_recon`). +* Updated unit and integration tests to reference the normalized plugin IDs. ### Deprecated @@ -20,7 +23,7 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver ### Removed -* None. +* Removed the duplicate `waf-detection` plugin (merged references and migrated legacy data to the `waf_detector` plugin). ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44991a18e..5c6f805cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -397,6 +397,9 @@ Please match the conventions already used in the repo instead of introducing a n - Use type hints where they improve clarity - Keep validation close to request and model boundaries - Prefer small functions over large, multi-purpose blocks +- Plugins: + - Plugin IDs must use `snake_case` (only lowercase letters, numbers, and underscores). Do not use hyphens. + - The `"id"` field in the plugin's `metadata.json` file must exactly match its directory name (e.g. `plugins/domain_finder/metadata.json` must have `"id": "domain_finder"`). - Frontend: - Use TypeScript and functional React components - Keep component logic readable and avoid unnecessary abstraction diff --git a/PLUGINS.md b/PLUGINS.md index a6263629c..43cc91c2e 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -9,12 +9,12 @@ New contributors should refer to these resources: This file is a human-readable index of the plugins currently present in `plugins/*/metadata.json`. -Last synced: 2026-05-11 +Last synced: 2026-06-10 ## At a Glance -- Total plugins: 59 -- Safe plugins: 26 +- Total plugins: 60 +- Safe plugins: 27 - Intrusive plugins: 25 - Exploit plugins: 8 - Source of truth: each plugin's `metadata.json` @@ -33,7 +33,7 @@ Only run scans against systems you own or are explicitly authorized to assess. | Category | Count | | --- | ---: | -| `recon` | 17 | +| `recon` | 18 | | `vulnerability` | 12 | | `robots` | 5 | | `web` | 5 | @@ -60,10 +60,10 @@ Only run scans against systems you own or are explicitly authorized to assess. | Directory Discovery | `dir_discovery` | `web` | `intrusive` | `ffuf` | Discover hidden directories and files on web servers. | | DNS Reconnaissance | `dns_enum` | `recon` | `safe` | `dnsrecon` | Enumerate DNS records and configurations. | | dnsx | `dnsx` | `recon` | `safe` | `dnsx` | DNS resolution and wildcard-aware validation at scale. | -| Domain Finder | `domain-finder` | `recon` | `safe` | `amass` | Discover additional domain names of target organization. | +| Domain Finder | `domain_finder` | `recon` | `safe` | `amass` | Discover additional domain names of target organization. | | Drupal Security Scan | `droopescan` | `vulnerability` | `intrusive` | `droopescan` | Drupal-focused CMS scanner for version and surface enumeration. | | Payload Fuzzer | `fuzzer` | `robots` | `exploit` | `python3` | Autonomously fuzz target fields with massive dictionaries. | -| Google Hacking | `google-dorking` | `recon` | `safe` | `python3` | Find publicly indexed information about target. | +| Google Hacking | `google_dorking` | `recon` | `safe` | `python3` | Find publicly indexed information about target. | | Password Recovery Audit | `hashcat` | `expert` | `exploit` | `hashcat` | Password recovery and hash audit workflow. | | HTTP Inspector | `http_inspector` | `web` | `safe` | `curl` | Inspect HTTP/HTTPS endpoints for headers, cookies, and TLS configuration. | | HTTP Request Logger | `http_request_logger` | `exploit` | `intrusive` | `httpx` | Handle incoming HTTP requests and record data. | @@ -79,8 +79,8 @@ Only run scans against systems you own or are explicitly authorized to assess. | Network Scanning | `nmap` | `network` | `safe` | `nmap` | Network discovery and port scanning tool. | | Template Vulnerability Scan | `nuclei` | `web` | `intrusive` | `nuclei` | Fast and customizable vulnerability scanner. | | Password Auditor | `password_auditor` | `vulnerability` | `intrusive` | `python3` | Discover weak credentials in network services and web apps. | -| People Hunter | `people-email-discovery` | `recon` | `safe` | `theHarvester` | Discover email addresses and social media profiles. | -| Port Scanner | `port-scanner` | `recon` | `intrusive` | `nmap` | Detect open ports and fingerprint services. | +| People Hunter | `people_email_discovery` | `recon` | `safe` | `theHarvester` | Discover email addresses and social media profiles. | +| Port Scanner | `port_scanner` | `recon` | `intrusive` | `nmap` | Detect open ports and fingerprint services. | | Advanced Network Recon | `scapy_recon` | `network` | `safe` | `python3` | Advanced network probing using Scapy. | | Secret Scanner | `secret_scanner` | `code` | `safe` | `gitleaks` | Scan directories for hardcoded secrets. | | Semgrep Scanner | `semgrep_scanner` | `code` | `safe` | `semgrep` | Multi-language static code analysis using Semgrep. | @@ -92,18 +92,20 @@ Only run scans against systems you own or are explicitly authorized to assess. | SQLi Exploiter | `sqli_exploiter` | `exploit` | `exploit` | `sqlmap` | Exploitation-focused workflow for data extraction from confirmed SQL injection findings. | | SQL Injection Testing | `sqlmap` | `web` | `exploit` | `sqlmap` | Detects SQL injection vulnerabilities and supports controlled database enumeration. | | SSH Runner | `ssh_runner` | `execution` | `intrusive` | `ssh` | Remote command execution via SSH. | +| Subdomain Finder | `subdomain_finder` | `recon` | `safe` | `subfinder` | Discover subdomains of a domain. | +| Subdomain Scanner | `subdomain_discovery` | `recon` | `safe` | `subfinder` | Enumerate subdomains using passive sources. | | Subdomain Discovery (Configurable) | `subdomain_discovery` | `recon` | `safe` | `subfinder` | Comprehensive configurable subdomain enumeration via passive sources. Thread count and source coverage tunable via presets. | | Subdomain Takeover | `subdomain_takeover` | `exploit` | `intrusive` | `subfinder` | Discover dangling DNS entries pointing to external services. | | Subfinder (Quick) | `subfinder` | `recon` | `safe` | `subfinder` | Quick passive subdomain enumeration with minimal configuration — just provide a root domain. | | theHarvester | `theharvester` | `recon` | `safe` | `theHarvester` | OSINT collection for emails, domains, and hosts. | | TLS Security Analysis | `tls_inspector` | `security` | `safe` | `openssl` | Examine TLS/SSL certificates and cipher configurations. | | Uncover | `uncover` | `recon` | `safe` | `uncover` | Discover internet-exposed assets from external search sources. | -| URL Fuzzer | `url-fuzzer-2` | `recon` | `intrusive` | `ffuf` | Discover hidden files and directories. | +| URL Fuzzer | `url_fuzzer` | `recon` | `intrusive` | `ffuf` | Discover hidden files and directories. | | urlfinder | `urlfinder` | `recon` | `safe` | `urlfinder` | Passive historical URL collection. | -| Virtual Hosts Finder | `virtual-host-finder` | `recon` | `intrusive` | `ffuf` | Find multiple websites hosted on the same server. | +| Virtual Hosts Finder | `virtual_host_finder` | `recon` | `intrusive` | `ffuf` | Find multiple websites hosted on the same server. | | Volatility | `volatility` | `forensics` | `intrusive` | `volatility3` | Memory forensics workflow using Volatility 3 plugins. | | WAF Detector | `waf_detector` | `robots` | `safe` | `wafw00f` | Automatically identify Web Application Firewalls protecting targets. | -| Website Recon | `website-recon-2` | `recon` | `safe` | `httpx` | Perform website reconnaissance focused on identifying web technologies, frameworks, and application stack details. | +| Website Recon | `website_recon` | `recon` | `safe` | `httpx` | Perform website reconnaissance focused on identifying web technologies, frameworks, and application stack details. | | Domain Registration Lookup | `whois_lookup` | `utils` | `safe` | `python3` | Domain registration information lookup. | | WordPress Security Scan | `wpscan` | `vulnerability` | `intrusive` | `wpscan` | WordPress security scanner for plugin, theme, and core risk visibility. | | XSS Exploiter | `xss_exploiter` | `exploit` | `exploit` | `python3` | Exploit XSS in real-life attacks to extract cookies and data. | diff --git a/testing/backend/integration/test_uncover_plugin.py b/testing/backend/integration/test_uncover_integration.py similarity index 100% rename from testing/backend/integration/test_uncover_plugin.py rename to testing/backend/integration/test_uncover_integration.py diff --git a/testing/backend/test_crawler_plugin.py b/testing/backend/test_crawler_plugin.py index 93ef65d9e..70419e84d 100644 --- a/testing/backend/test_crawler_plugin.py +++ b/testing/backend/test_crawler_plugin.py @@ -87,10 +87,11 @@ def test_crawler_target_field_requires_http_url(): data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")) fields = {f["id"]: f for f in data["fields"]} target_validation = fields["target"].get("validation", {}) - pattern = target_validation.get("pattern", "") - assert "https?" in pattern or "http" in pattern, ( + uses_preset = target_validation.get("validation_type") == "url" + uses_pattern = "https?" in target_validation.get("pattern", "") or \ + "http" in target_validation.get("pattern", "") + assert uses_preset or uses_pattern, \ "target field must validate for HTTP(S) URL format" - ) def test_crawler_has_optional_depth_field_with_default(): diff --git a/testing/backend/test_domain_finder_plugin.py b/testing/backend/test_domain_finder_plugin.py index a84c352eb..37599dd48 100644 --- a/testing/backend/test_domain_finder_plugin.py +++ b/testing/backend/test_domain_finder_plugin.py @@ -25,7 +25,7 @@ from backend.secuscan.plugin_validator import PluginMetadataValidator from backend.secuscan.plugins import PluginManager -PLUGIN_DIR = REPO_ROOT / "plugins" / "domain-finder" +PLUGIN_DIR = REPO_ROOT / "plugins" / "domain_finder" PLUGINS_DIR = REPO_ROOT / "plugins" # Import parser from domain-finder (hyphenated directory name requires importlib) @@ -65,7 +65,7 @@ def test_domain_finder_passes_validator(): def test_domain_finder_metadata_id_matches_directory(): """Plugin id in metadata.json must match the directory name.""" data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")) - assert data["id"] == "domain-finder" + assert data["id"] == "domain_finder" def test_domain_finder_engine_is_amass(): @@ -106,7 +106,7 @@ def test_domain_finder_command_renders_with_target(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("domain-finder", {"target": "secuscan.in"}) + command = manager.build_command("domain_finder", {"target": "secuscan.in"}) assert command is not None, "build_command returned None for valid inputs" assert command[0] == "amass" @@ -123,7 +123,7 @@ def test_domain_finder_command_full_token_sequence(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("domain-finder", {"target": "secuscan.in"}) + command = manager.build_command("domain_finder", {"target": "secuscan.in"}) assert command == [ "amass", @@ -141,9 +141,9 @@ def test_domain_finder_loaded_by_plugin_manager(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - plugin = manager.get_plugin("domain-finder") + plugin = manager.get_plugin("domain_finder") assert plugin is not None - assert plugin.id == "domain-finder" + assert plugin.id == "domain_finder" assert plugin.name == "Domain Finder" diff --git a/testing/backend/test_google_dorking_plugin.py b/testing/backend/test_google_dorking_plugin.py index e6d872a34..0e4d4b360 100644 --- a/testing/backend/test_google_dorking_plugin.py +++ b/testing/backend/test_google_dorking_plugin.py @@ -1,14 +1,14 @@ """ -Contract and parser tests for the google-dorking plugin. +Contract and parser tests for the google_dorking plugin. -These tests load the real plugins/google-dorking/metadata.json, validate it +These tests load the real plugins/google_dorking/metadata.json, validate it through the project PluginMetadataValidator, render commands through the real PluginManager, and call the real parser.py parse() function. Assertions are tied to the actual plugin contract: if metadata.json, the command template, or parser.py drift, these tests will fail. -Related to issue #498: Add parser and contract coverage for plugin `google-dorking` +Related to issue #498: Add parser and contract coverage for plugin `google_dorking` """ import asyncio @@ -26,17 +26,17 @@ from backend.secuscan.plugins import PluginManager # --------------------------------------------------------------------------- -# Load parser from hyphenated directory name +# Load parser from directory name # --------------------------------------------------------------------------- _spec = importlib.util.spec_from_file_location( "google_dorking_parser", - REPO_ROOT / "plugins" / "google-dorking" / "parser.py" + REPO_ROOT / "plugins" / "google_dorking" / "parser.py" ) _mod = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_mod) parse = _mod.parse -PLUGIN_DIR = REPO_ROOT / "plugins" / "google-dorking" +PLUGIN_DIR = REPO_ROOT / "plugins" / "google_dorking" PLUGINS_DIR = REPO_ROOT / "plugins" @@ -64,7 +64,7 @@ def test_google_dorking_passes_validator(): def test_google_dorking_metadata_id_matches_directory(): data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")) - assert data["id"] == "google-dorking" + assert data["id"] == "google_dorking" def test_google_dorking_engine_is_python3(): @@ -107,16 +107,16 @@ def test_google_dorking_category_is_recon(): def test_google_dorking_loaded_by_plugin_manager(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - plugin = manager.get_plugin("google-dorking") + plugin = manager.get_plugin("google_dorking") assert plugin is not None - assert plugin.id == "google-dorking" + assert plugin.id == "google_dorking" assert plugin.name == "Google Hacking" def test_google_dorking_command_renders_with_target(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("google-dorking", {"target": "secuscan.in"}) + command = manager.build_command("google_dorking", {"target": "secuscan.in"}) assert command is not None assert command[0] == "python3" assert "secuscan.in" in " ".join(command) @@ -125,7 +125,7 @@ def test_google_dorking_command_renders_with_target(setup_test_environment): def test_google_dorking_command_contains_dork_queries(setup_test_environment): manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("google-dorking", {"target": "secuscan.in"}) + command = manager.build_command("google_dorking", {"target": "secuscan.in"}) full_command = " ".join(command) assert "site:" in full_command assert "inurl:admin" in full_command diff --git a/testing/backend/test_people_email_discovery_plugin.py b/testing/backend/test_people_email_discovery_plugin.py index 89d6eb31c..a8e9da7a2 100644 --- a/testing/backend/test_people_email_discovery_plugin.py +++ b/testing/backend/test_people_email_discovery_plugin.py @@ -25,7 +25,7 @@ from backend.secuscan.plugins import PluginManager from plugins.people_email_discovery.parser import parse -PLUGIN_DIR = REPO_ROOT / "plugins" / "people-email-discovery" +PLUGIN_DIR = REPO_ROOT / "plugins" / "people_email_discovery" PLUGINS_DIR = REPO_ROOT / "plugins" @@ -64,7 +64,7 @@ def test_people_email_discovery_passes_validator(): def test_people_email_discovery_metadata_id_matches_directory(): """Plugin id in metadata.json must match the directory name.""" data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")) - assert data["id"] == "people-email-discovery" + assert data["id"] == "people_email_discovery" def test_people_email_discovery_engine_is_theHarvester(): @@ -108,7 +108,7 @@ def test_people_email_discovery_command_renders_with_target(setup_test_environme manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("people-email-discovery", {"target": "example.com"}) + command = manager.build_command("people_email_discovery", {"target": "example.com"}) assert command is not None, "build_command returned None for valid inputs" assert "theHarvester" in command @@ -123,7 +123,7 @@ def test_people_email_discovery_command_full_token_sequence(setup_test_environme manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - command = manager.build_command("people-email-discovery", {"target": "secuscan.in"}) + command = manager.build_command("people_email_discovery", {"target": "secuscan.in"}) assert command == ["theHarvester", "-d", "secuscan.in", "-b", "all"], ( f"Command template drift detected. Got: {command}" @@ -135,9 +135,9 @@ def test_people_email_discovery_loaded_by_plugin_manager(setup_test_environment) manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - plugin = manager.get_plugin("people-email-discovery") + plugin = manager.get_plugin("people_email_discovery") assert plugin is not None - assert plugin.id == "people-email-discovery" + assert plugin.id == "people_email_discovery" assert plugin.name == "People Hunter" @@ -198,4 +198,4 @@ def test_people_email_discovery_parser_preserves_raw_line_in_metadata(): single_line = "admin@example.com\n" result = parse(single_line) assert result["findings"] - assert result["findings"][0]["metadata"]["raw"] == "admin@example.com" + assert result["findings"][0]["metadata"]["raw_line"] == "admin@example.com" diff --git a/testing/backend/test_subdomain_finder_plugin.py b/testing/backend/test_subdomain_finder_plugin.py index f550a9279..8e835df07 100644 --- a/testing/backend/test_subdomain_finder_plugin.py +++ b/testing/backend/test_subdomain_finder_plugin.py @@ -12,7 +12,7 @@ from backend.secuscan.plugin_validator import PluginMetadataValidator from backend.secuscan.plugins import PluginManager -PLUGIN_ID = "subdomain-finder" +PLUGIN_ID = "subdomain_finder" PLUGIN_DIR = REPO_ROOT / "plugins" / PLUGIN_ID PLUGINS_DIR = REPO_ROOT / "plugins" PARSER_PATH = PLUGIN_DIR / "parser.py" diff --git a/testing/backend/test_website_recon_2_plugin.py b/testing/backend/test_website_recon_2_plugin.py index 4a2b3f60a..3f5d41047 100644 --- a/testing/backend/test_website_recon_2_plugin.py +++ b/testing/backend/test_website_recon_2_plugin.py @@ -22,7 +22,7 @@ from backend.secuscan.plugin_validator import PluginMetadataValidator from backend.secuscan.plugins import PluginManager -PLUGIN_DIR = REPO_ROOT / "plugins" / "website-recon-2" +PLUGIN_DIR = REPO_ROOT / "plugins" / "website_recon" PLUGINS_DIR = REPO_ROOT / "plugins" # --------------------------------------------------------------------------- @@ -64,7 +64,7 @@ def test_website_recon_2_metadata_id_matches_directory(): (PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8") ) - assert data["id"] == "website-recon-2" + assert data["id"] == "website_recon" def test_website_recon_2_engine_is_httpx(): data = json.loads( @@ -105,7 +105,7 @@ def test_website_recon_2_command_renders_with_target( asyncio.run(manager.load_plugins()) command = manager.build_command( - "website-recon-2", + "website_recon", {"target": "https://secuscan.in"}, ) @@ -126,7 +126,7 @@ def test_website_recon_2_command_full_token_sequence( asyncio.run(manager.load_plugins()) command = manager.build_command( - "website-recon-2", + "website_recon", {"target": "https://secuscan.in"}, ) @@ -147,7 +147,7 @@ def test_website_recon_2_drops_target_token_when_absent( asyncio.run(manager.load_plugins()) rendered = manager.build_command( - "website-recon-2", + "website_recon", {}, ) @@ -155,7 +155,7 @@ def test_website_recon_2_drops_target_token_when_absent( assert not any("{" in token for token in rendered) populated = manager.build_command( - "website-recon-2", + "website_recon", {"target": "https://secuscan.in"}, ) @@ -167,10 +167,10 @@ def test_website_recon_2_loaded_by_plugin_manager( manager = PluginManager(str(PLUGINS_DIR)) asyncio.run(manager.load_plugins()) - plugin = manager.get_plugin("website-recon-2") + plugin = manager.get_plugin("website_recon") assert plugin is not None - assert plugin.id == "website-recon-2" + assert plugin.id == "website_recon" # --------------------------------------------------------------------------- # Parser contract tests diff --git a/testing/backend/unit/test_plugin_compatibility.py b/testing/backend/unit/test_plugin_compatibility.py new file mode 100644 index 000000000..90f016fe8 --- /dev/null +++ b/testing/backend/unit/test_plugin_compatibility.py @@ -0,0 +1,71 @@ +import asyncio +import pytest +from backend.secuscan.config import settings +from backend.secuscan.plugins import PluginManager, LEGACY_PLUGIN_ID_ALIASES + +def test_legacy_plugin_id_aliases_resolve(setup_test_environment): + """Test that all defined legacy plugin ID aliases successfully resolve to valid, loaded plugins.""" + manager = PluginManager(settings.plugins_dir) + asyncio.run(manager.load_plugins()) + + for legacy_id, canonical_id in LEGACY_PLUGIN_ID_ALIASES.items(): + # Get plugin metadata by legacy ID + plugin_by_legacy = manager.get_plugin(legacy_id) + assert plugin_by_legacy is not None, f"Legacy ID {legacy_id} returned None" + assert plugin_by_legacy.id == canonical_id, f"Legacy ID {legacy_id} resolved to {plugin_by_legacy.id} instead of {canonical_id}" + + # Get plugin metadata by canonical ID + plugin_by_canonical = manager.get_plugin(canonical_id) + assert plugin_by_canonical is not None, f"Canonical ID {canonical_id} returned None" + assert plugin_by_legacy == plugin_by_canonical + + # Get schema by legacy and canonical ID + schema_by_legacy = manager.get_plugin_schema(legacy_id) + schema_by_canonical = manager.get_plugin_schema(canonical_id) + assert schema_by_legacy == schema_by_canonical + assert schema_by_legacy["id"] == canonical_id + +def test_build_command_with_legacy_id(setup_test_environment): + """Test that building commands with legacy IDs works and generates the exact same command as the canonical ID.""" + manager = PluginManager(settings.plugins_dir) + asyncio.run(manager.load_plugins()) + + # Test subdomain-finder + cmd_legacy = manager.build_command("subdomain-finder", {"target": "example.com"}) + cmd_canonical = manager.build_command("subdomain_finder", {"target": "example.com"}) + assert cmd_legacy == cmd_canonical + assert cmd_legacy is not None + assert "subfinder" in cmd_legacy + assert "example.com" in cmd_legacy + + # Test google-dorking + cmd_legacy = manager.build_command("google-dorking", {"target": "example.com"}) + cmd_canonical = manager.build_command("google_dorking", {"target": "example.com"}) + assert cmd_legacy == cmd_canonical + assert cmd_legacy is not None + assert "google" in cmd_legacy or "python3" in cmd_legacy + +def test_task_start_via_api_resolves_legacy_id(test_client): + """Test that submitting a task with a legacy plugin ID via the API succeeds and stores the task with the canonical ID.""" + # Submit task with legacy ID "subdomain-finder" + response = test_client.post( + "/api/v1/task/start", + json={ + "plugin_id": "subdomain-finder", + "consent_granted": True, + "inputs": { + "target": "127.0.0.1" + } + } + ) + assert response.status_code == 200, f"Task start failed: {response.text}" + data = response.json() + assert "task_id" in data + task_id = data["task_id"] + + # Verify task details via the status API + status_response = test_client.get(f"/api/v1/task/{task_id}/status") + assert status_response.status_code == 200 + status_data = status_response.json() + # The plugin_id stored on the task must be the canonical/standardized one + assert status_data["plugin_id"] == "subdomain_finder" diff --git a/testing/backend/unit/test_plugin_validator.py b/testing/backend/unit/test_plugin_validator.py index e153d972e..fc0d343a6 100644 --- a/testing/backend/unit/test_plugin_validator.py +++ b/testing/backend/unit/test_plugin_validator.py @@ -48,7 +48,8 @@ def _error_messages(result: ValidationResult) -> list[str]: def _write_metadata(tmp_path: Path, data: dict) -> Path: - plugin_dir = tmp_path / "my_plugin" + dir_name = data.get("id") or "my_plugin" + plugin_dir = tmp_path / dir_name plugin_dir.mkdir(exist_ok=True) (plugin_dir / "metadata.json").write_text(json.dumps(data), encoding="utf-8") return plugin_dir @@ -168,6 +169,41 @@ def test_empty_string_name_is_reported(self, tmp_path): result = validate_one_plugin(plugin_dir) assert "name" in _error_paths(result) +# =========================================================================== +# Plugin ID validation +# =========================================================================== + +class TestPluginId: + @pytest.mark.parametrize("valid_id", [ + "amass", "api_scanner", "dns_enum", "http_inspector", "waf_detector", "website_recon", "fuzzer", "tls_inspector_v2" + ]) + def test_valid_ids_accepted(self, tmp_path, valid_id): + data = _minimal_valid() + data["id"] = valid_id + plugin_dir = _write_metadata(tmp_path, data) + result = validate_one_plugin(plugin_dir) + assert "id" not in _error_paths(result) + + @pytest.mark.parametrize("invalid_id", [ + "domain-finder", "google-dorking", "people-email-discovery", "port-scanner", "subdomain-finder", "url-fuzzer-2", "website-recon-2", "DNS_Enum", "tls inspector", "123_scanner", "_scanner" + ]) + def test_invalid_ids_rejected(self, tmp_path, invalid_id): + data = _minimal_valid() + data["id"] = invalid_id + plugin_dir = _write_metadata(tmp_path, data) + result = validate_one_plugin(plugin_dir) + assert "id" in _error_paths(result) + + def test_mismatched_directory_name_rejected(self, tmp_path): + data = _minimal_valid() + data["id"] = "test_ping" + # Manually write to mismatched folder name + plugin_dir = tmp_path / "mismatched_folder" + plugin_dir.mkdir() + (plugin_dir / "metadata.json").write_text(json.dumps(data), encoding="utf-8") + result = validate_one_plugin(plugin_dir) + assert "id" in _error_paths(result) + assert any("directory name" in e.message for e in result.errors if e.path == "id") # =========================================================================== # Engine diff --git a/testing/backend/unit/test_plugins.py b/testing/backend/unit/test_plugins.py index 70a559b3d..32c97c2a9 100644 --- a/testing/backend/unit/test_plugins.py +++ b/testing/backend/unit/test_plugins.py @@ -343,7 +343,7 @@ def test_plugin_manager_rejects_linux_wordlist_absolute_default(setup_test_envir with pytest.raises(ValueError, match="absolute"): manager.build_command( - "virtual-host-finder", + "virtual_host_finder", {"target": "example.com"}, ) diff --git a/testing/backend/unit/test_port_scanner_plugin.py b/testing/backend/unit/test_port_scanner_plugin.py index e8aeceae6..1a6d2817e 100644 --- a/testing/backend/unit/test_port_scanner_plugin.py +++ b/testing/backend/unit/test_port_scanner_plugin.py @@ -9,7 +9,7 @@ import pytest # Import the parser module directly from the file without requiring __init__.py -_parser_path = Path(__file__).resolve().parents[3] / "plugins" / "port-scanner" / "parser.py" +_parser_path = Path(__file__).resolve().parents[3] / "plugins" / "port_scanner" / "parser.py" _spec = importlib.util.spec_from_file_location("plugins.port_scanner.parser", str(_parser_path)) _parser_module = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_parser_module) diff --git a/testing/backend/unit/test_subdomain_finder_plugin.py b/testing/backend/unit/test_subdomain_discovery_plugin.py similarity index 100% rename from testing/backend/unit/test_subdomain_finder_plugin.py rename to testing/backend/unit/test_subdomain_discovery_plugin.py diff --git a/testing/backend/unit/test_url_fuzzer_2_plugin.py b/testing/backend/unit/test_url_fuzzer_2_plugin.py index 8a0a8642d..e28f56293 100644 --- a/testing/backend/unit/test_url_fuzzer_2_plugin.py +++ b/testing/backend/unit/test_url_fuzzer_2_plugin.py @@ -5,7 +5,7 @@ from backend.secuscan.config import settings from backend.secuscan.plugins import PluginManager -PLUGIN_ID = "url-fuzzer-2" +PLUGIN_ID = "url_fuzzer" PLUGIN_DIR = Path(settings.plugins_dir) / PLUGIN_ID diff --git a/testing/backend/unit/test_virtual_host_finder_plugin.py b/testing/backend/unit/test_virtual_host_finder_plugin.py index 10cf1c4b6..a9fc59463 100644 --- a/testing/backend/unit/test_virtual_host_finder_plugin.py +++ b/testing/backend/unit/test_virtual_host_finder_plugin.py @@ -8,7 +8,7 @@ import pytest # Import the parser module directly from the file without requiring __init__.py -_parser_path = Path(__file__).resolve().parents[3] / "plugins" / "virtual-host-finder" / "parser.py" +_parser_path = Path(__file__).resolve().parents[3] / "plugins" / "virtual_host_finder" / "parser.py" _spec = importlib.util.spec_from_file_location("plugins.virtual_host_finder.parser", str(_parser_path)) _parser_module = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_parser_module) From 3c878d676d0fa42807e55fcb960d057b3797847e Mon Sep 17 00:00:00 2001 From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:46:41 +0530 Subject: [PATCH 4/4] fix(plugins): add missing capabilities declaration to subdomain_finder --- plugins/subdomain_finder/metadata.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/subdomain_finder/metadata.json b/plugins/subdomain_finder/metadata.json index 21b5462c1..383d5a52c 100644 --- a/plugins/subdomain_finder/metadata.json +++ b/plugins/subdomain_finder/metadata.json @@ -52,5 +52,8 @@ "python_packages": [], "system_packages": [] }, - "checksum": "964933cf6099bb2d5713ed09da228efdaa5dd295c0015690c5319485873c061c" + "capabilities": [ + "network" + ], + "checksum": "b8f40154cef9219bd31bb9469cfc1ae7cbfbe3255766a7d43c0716909294df07" }