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/backend/data/.api_key b/backend/data/.api_key index 9cb63f9f5..d9fc1946f 100644 --- a/backend/data/.api_key +++ b/backend/data/.api_key @@ -1 +1 @@ -eabf1047a50524ac618dc236736f35aa35f85920d158648aabae6f773b8a93c5 \ No newline at end of file +eabf1047a50524ac618dc236736f35aa35f85920d158648aabae6f773b8a93c5 diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index fad732c2b..9a67b49bb 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 41d26bd47..784fa3ff9 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), @@ -318,8 +330,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""" @@ -522,7 +535,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 4e2b73f89..0816b56e3 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -428,7 +428,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, ) @@ -439,7 +439,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, diff --git a/plugins/cloud_scanner/metadata.json b/plugins/cloud_scanner/metadata.json index a34b80e94..9d687502e 100644 --- a/plugins/cloud_scanner/metadata.json +++ b/plugins/cloud_scanner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/code_analyzer/metadata.json b/plugins/code_analyzer/metadata.json index 6b8c4a487..fff5d8ce5 100644 --- a/plugins/code_analyzer/metadata.json +++ b/plugins/code_analyzer/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udcc3", + "icon": "๐Ÿ“ƒ", "engine": { "type": "cli", "binary": "bandit" diff --git a/plugins/container_scanner/metadata.json b/plugins/container_scanner/metadata.json index 792f54766..d6024280c 100644 --- a/plugins/container_scanner/metadata.json +++ b/plugins/container_scanner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "Apache-2.0", - "icon": "\ud83d\udce6", + "icon": "๐Ÿ“ฆ", "engine": { "type": "cli", "binary": "trivy" diff --git a/plugins/dir_discovery/metadata.json b/plugins/dir_discovery/metadata.json index 0449cfacb..690018a6c 100644 --- a/plugins/dir_discovery/metadata.json +++ b/plugins/dir_discovery/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udcc2", + "icon": "๐Ÿ“‚", "engine": { "type": "cli", "binary": "ffuf" diff --git a/plugins/dns_enum/metadata.json b/plugins/dns_enum/metadata.json index 962246e84..5d19643c4 100644 --- a/plugins/dns_enum/metadata.json +++ b/plugins/dns_enum/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udce6", + "icon": "๐Ÿ“ฆ", "engine": { "type": "cli", "binary": "dnsrecon" diff --git a/plugins/domain-finder/metadata.json b/plugins/domain_finder/metadata.json similarity index 88% rename from plugins/domain-finder/metadata.json rename to plugins/domain_finder/metadata.json index 00e40a60f..eaa1a803d 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "amass" @@ -55,5 +55,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "b8bc0ebc7ba6b739c3f81be706c98cc354e9ffdad1468d62ef00600a3e8907d6" + "checksum": "8b37efdcea455b696de31c990167cc39d9040472136170ed95449d4acfdcecb0" } 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/fuzzer/metadata.json b/plugins/fuzzer/metadata.json index da88c9311..0bd0aacab 100644 --- a/plugins/fuzzer/metadata.json +++ b/plugins/fuzzer/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/google-dorking/metadata.json b/plugins/google_dorking/metadata.json similarity index 89% rename from plugins/google-dorking/metadata.json rename to plugins/google_dorking/metadata.json index 8848acf27..aed67196b 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "python3" @@ -52,5 +52,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "7284033396542bcc591307d2339b525d7eebd0da5365a252b79ff21a0160f208" + "checksum": "f2665f8e61c02957c0947a2df508a96895e1df5554a4a9942bf747c1c01b262f" } 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/hashcat/metadata.json b/plugins/hashcat/metadata.json index 91e6878a1..faabf2132 100644 --- a/plugins/hashcat/metadata.json +++ b/plugins/hashcat/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\u26a1", + "icon": "โšก", "engine": { "type": "cli", "binary": "hashcat" 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/iac_scanner/metadata.json b/plugins/iac_scanner/metadata.json index e8346a24b..645117515 100644 --- a/plugins/iac_scanner/metadata.json +++ b/plugins/iac_scanner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/icmp_ping/metadata.json b/plugins/icmp_ping/metadata.json index f1d96bfdb..52a38181e 100644 --- a/plugins/icmp_ping/metadata.json +++ b/plugins/icmp_ping/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "ping" diff --git a/plugins/kubernetes_scanner/metadata.json b/plugins/kubernetes_scanner/metadata.json index 04c597059..8874c9843 100644 --- a/plugins/kubernetes_scanner/metadata.json +++ b/plugins/kubernetes_scanner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/metasploit/metadata.json b/plugins/metasploit/metadata.json index 02047fbd7..4aa68b600 100644 --- a/plugins/metasploit/metadata.json +++ b/plugins/metasploit/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\ude80", + "icon": "๐Ÿš€", "engine": { "type": "cli", "binary": "msfconsole" diff --git a/plugins/nuclei/metadata.json b/plugins/nuclei/metadata.json index bf669ba9e..8070e26b0 100644 --- a/plugins/nuclei/metadata.json +++ b/plugins/nuclei/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83e\uddec", + "icon": "๐Ÿงฌ", "engine": { "type": "cli", "binary": "nuclei" diff --git a/plugins/people-email-discovery/metadata.json b/plugins/people_email_discovery/metadata.json similarity index 89% rename from plugins/people-email-discovery/metadata.json rename to plugins/people_email_discovery/metadata.json index 8ae654f09..076bd8e2f 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "theHarvester" @@ -54,5 +54,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "b637adaf26113ae2e6225ad26b61bc60bd3777a6c3e8fac58d0ec78b21ebd4e1" + "checksum": "0d095166c27f22e7aef301284dd3f55baef28e0b67bf0550f3f4b64a46278293" } 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 91% rename from plugins/port-scanner/metadata.json rename to plugins/port_scanner/metadata.json index 1aeb5e6e8..54911c29f 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "nmap" @@ -67,5 +67,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "1fe95f15404deaa261d64f891dcac4e87bc4dc4c3d4ca4fb7c526bdcdcf2c762" + "checksum": "77e6ec0d78b0f89e645a297f4765eb6ed0b99da6004508458478b8e31535f12e" } 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/scapy_recon/metadata.json b/plugins/scapy_recon/metadata.json index 947907b6c..e1c66f405 100644 --- a/plugins/scapy_recon/metadata.json +++ b/plugins/scapy_recon/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udce1", + "icon": "๐Ÿ“ก", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/secret_scanner/metadata.json b/plugins/secret_scanner/metadata.json index 1dd98e849..3f5a90d55 100644 --- a/plugins/secret_scanner/metadata.json +++ b/plugins/secret_scanner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd11", + "icon": "๐Ÿ”‘", "engine": { "type": "cli", "binary": "gitleaks" diff --git a/plugins/sqli_checker/metadata.json b/plugins/sqli_checker/metadata.json index 194445018..c333a4263 100644 --- a/plugins/sqli_checker/metadata.json +++ b/plugins/sqli_checker/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83e\uddea", + "icon": "๐Ÿงช", "engine": { "type": "cli", "binary": "ghauri" diff --git a/plugins/sqli_exploiter/metadata.json b/plugins/sqli_exploiter/metadata.json index 37dfc95fa..696d43f6a 100644 --- a/plugins/sqli_exploiter/metadata.json +++ b/plugins/sqli_exploiter/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "sqlmap" diff --git a/plugins/sqlmap/metadata.json b/plugins/sqlmap/metadata.json index 664439259..14fa4af9a 100644 --- a/plugins/sqlmap/metadata.json +++ b/plugins/sqlmap/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "GPLv2", - "icon": "\ud83d\udc89", + "icon": "๐Ÿ’‰", "engine": { "type": "cli", "binary": "sqlmap" diff --git a/plugins/ssh_runner/metadata.json b/plugins/ssh_runner/metadata.json index a0af53824..ea20b3933 100644 --- a/plugins/ssh_runner/metadata.json +++ b/plugins/ssh_runner/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udda5\ufe0f", + "icon": "๐Ÿ–ฅ๏ธ", "engine": { "type": "cli", "binary": "ssh" diff --git a/plugins/subdomain_discovery/metadata.json b/plugins/subdomain_discovery/metadata.json index c8b9d49f1..aae6bbeea 100644 --- a/plugins/subdomain_discovery/metadata.json +++ b/plugins/subdomain_discovery/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83c\udf10", + "icon": "๐ŸŒ", "engine": { "type": "cli", "binary": "subfinder" 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/subdomain_takeover/metadata.json b/plugins/subdomain_takeover/metadata.json index 3999e311b..0c02343c2 100644 --- a/plugins/subdomain_takeover/metadata.json +++ b/plugins/subdomain_takeover/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udee0\ufe0f", + "icon": "๐Ÿ› ๏ธ", "engine": { "type": "cli", "binary": "subfinder" diff --git a/plugins/subfinder/metadata.json b/plugins/subfinder/metadata.json index 4d0c0bc3e..dacecc360 100644 --- a/plugins/subfinder/metadata.json +++ b/plugins/subfinder/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "subfinder" diff --git a/plugins/theharvester/metadata.json b/plugins/theharvester/metadata.json index bde2cdd9f..8abd80a0f 100644 --- a/plugins/theharvester/metadata.json +++ b/plugins/theharvester/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "theHarvester" diff --git a/plugins/tls_inspector/metadata.json b/plugins/tls_inspector/metadata.json index eace0c65a..c3c01af8c 100644 --- a/plugins/tls_inspector/metadata.json +++ b/plugins/tls_inspector/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd10", + "icon": "๐Ÿ”", "engine": { "type": "cli", "binary": "openssl" diff --git a/plugins/uncover/metadata.json b/plugins/uncover/metadata.json index e9b744271..a1e140a81 100644 --- a/plugins/uncover/metadata.json +++ b/plugins/uncover/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "uncover" diff --git a/plugins/url-fuzzer-2/metadata.json b/plugins/url_fuzzer/metadata.json similarity index 92% rename from plugins/url-fuzzer-2/metadata.json rename to plugins/url_fuzzer/metadata.json index ebe151ba7..846ad9bf5 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "ffuf" @@ -74,5 +74,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "4fada9f7e171899e44ddccdb66be22500f086a7afe1c69b46a01c16e5803e66c" + "checksum": "fbe8df1febf08a7280a74abeccc4fff1c845134110aecf7b345c580b032ad5b1" } 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/urlfinder/metadata.json b/plugins/urlfinder/metadata.json index d3c063acb..9645ddbf9 100644 --- a/plugins/urlfinder/metadata.json +++ b/plugins/urlfinder/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "urlfinder" diff --git a/plugins/virtual-host-finder/metadata.json b/plugins/virtual_host_finder/metadata.json similarity index 92% rename from plugins/virtual-host-finder/metadata.json rename to plugins/virtual_host_finder/metadata.json index 34aa394b6..27e228a78 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.", @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "ffuf" @@ -68,5 +68,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "fb787f5f51da9f62c7a87d04694d5fa8624b0cf656f0d464436c7a8de45a58df" + "checksum": "07e98a178e6619427d7ac08cc3e3c0b717a202c0bf241ca37f8de0774645b006" } 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 b1ad20a7f..ddf9f1863 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.", @@ -60,5 +60,5 @@ "python_packages": [], "system_packages": [] }, - "checksum": "4f8cf37fc9c3de4cdab5d4cb140c619f11bbf76ca6926aa6c41288168e8637f4" + "checksum": "825b4388ea96b666a35a4c5d9a11c9c9346754878c5146806992dde67999225b" } 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 diff --git a/plugins/whois_lookup/metadata.json b/plugins/whois_lookup/metadata.json index 92d438df5..f37804bba 100644 --- a/plugins/whois_lookup/metadata.json +++ b/plugins/whois_lookup/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.in" }, "license": "MIT", - "icon": "\ud83d\udd0e", + "icon": "๐Ÿ”Ž", "engine": { "type": "cli", "binary": "python3" diff --git a/plugins/wpscan/metadata.json b/plugins/wpscan/metadata.json index 25d651514..ae3f05497 100644 --- a/plugins/wpscan/metadata.json +++ b/plugins/wpscan/metadata.json @@ -10,7 +10,7 @@ "email": "dev@secuscan.local" }, "license": "MIT", - "icon": "\ud83d\udcdd", + "icon": "๐Ÿ“", "engine": { "type": "cli", "binary": "wpscan" 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 4d3db8767..35366c9bc 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 8fc249dbf..818b9a697 100644 --- a/testing/backend/unit/test_plugins.py +++ b/testing/backend/unit/test_plugins.py @@ -300,7 +300,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)