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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion backend/secuscan/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ async def create_task(
if not plugin:
raise ValueError(f"Plugin not found: {plugin_id}")

from backend.secuscan.plugins import LEGACY_PLUGIN_ID_ALIASES
plugin_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin.id, plugin.id)

# Apply preset if provided
if preset and preset in plugin.presets:
preset_values = plugin.presets[preset]
Expand Down Expand Up @@ -1671,7 +1674,10 @@ def _parse_results(self, plugin, output: str) -> Dict[str, Any]:

# 1. Check for custom parser.py in plugin directory (Recommended)
plugin_manager = get_plugin_manager()
plugin_dir = plugin_manager.plugins_dir / plugin.id
if hasattr(plugin_manager, "resolve_plugin_dir") and not hasattr(plugin_manager.resolve_plugin_dir, "called"):
plugin_dir = plugin_manager.resolve_plugin_dir(plugin.id)
else:
plugin_dir = plugin_manager.plugins_dir / plugin.id
parser_path = plugin_dir / "parser.py"

if parser_path.exists():
Expand Down
79 changes: 79 additions & 0 deletions backend/secuscan/migrations/007_standardize_plugin_ids.sql
Original file line number Diff line number Diff line change
@@ -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';
22 changes: 22 additions & 0 deletions backend/secuscan/plugin_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -137,6 +139,26 @@ 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

from backend.secuscan.plugins import LEGACY_PLUGIN_ID_ALIASES
if plugin_id_value in LEGACY_PLUGIN_ID_ALIASES or plugin_id_value in LEGACY_PLUGIN_ID_ALIASES.values():
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:
Expand Down
54 changes: 49 additions & 5 deletions backend/secuscan/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -359,22 +371,54 @@ 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/standardized aliases in both directions."""
# 1. Direct lookup
if plugin := self.plugins.get(plugin_id):
return plugin
# 2. If legacy ID was queried but it is loaded under standardized ID
standardized_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin_id)
if standardized_id and standardized_id in self.plugins:
return self.plugins[standardized_id]
# 3. If standardized ID was queried but it is loaded under legacy ID
for legacy_id, std_id in LEGACY_PLUGIN_ID_ALIASES.items():
if std_id == plugin_id:
if plugin := self.plugins.get(legacy_id):
return plugin
return None

def resolve_plugin_dir(self, plugin_id: str) -> Path:
"""Resolve the plugin's directory on disk, supporting both standardized and legacy names."""
# 1. Try exact match
std_dir = self.plugins_dir / plugin_id
if std_dir.exists():
return std_dir
# 2. Try mapping standard to legacy
reverse_aliases = {v: k for k, v in LEGACY_PLUGIN_ID_ALIASES.items()}
legacy_id = reverse_aliases.get(plugin_id, plugin_id)
legacy_dir = self.plugins_dir / legacy_id
if legacy_dir.exists():
return legacy_dir
# 3. Try mapping legacy to standard
mapped_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin_id, plugin_id)
mapped_dir = self.plugins_dir / mapped_id
if mapped_dir.exists():
return mapped_dir
return std_dir

def list_plugins(self) -> List[Dict]:
"""List all loaded plugins"""
plugins: List[Dict] = []
for plugin in self.plugins.values():
missing_binaries = self._get_missing_binaries(plugin)
standardized_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin.id, plugin.id)
plugins.append(
{
"id": plugin.id,
"id": standardized_id,
"name": plugin.name,
"description": plugin.description,
"category": plugin.category,
"safety_level": plugin.safety.get("level"),
"enabled": plugin.id not in settings.disabled_plugins,
"enabled": standardized_id not in settings.disabled_plugins,
"icon": plugin.icon,
"requires_consent": bool(plugin.safety.get("requires_consent", False)),
"consent_message": plugin.safety.get("consent_message"),
Expand Down Expand Up @@ -421,7 +465,7 @@ def get_plugin_schema(self, plugin_id: str) -> Optional[Dict]:
"""Get full plugin schema for UI generation"""
if plugin := self.get_plugin(plugin_id):
return {
"id": plugin.id,
"id": LEGACY_PLUGIN_ID_ALIASES.get(plugin.id, plugin.id),
"name": plugin.name,
"description": plugin.description,
"fields": [f.model_dump() for f in plugin.fields],
Expand Down
6 changes: 4 additions & 2 deletions backend/secuscan/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,10 @@ async def start_task(
# Check rate limits per (client, plugin) so one client cannot exhaust
# the quota for all other users of the same plugin.
client_id = resolve_client_identity(raw_request)
from backend.secuscan.plugins import LEGACY_PLUGIN_ID_ALIASES
standardized_id = LEGACY_PLUGIN_ID_ALIASES.get(plugin.id, plugin.id)
can_execute, error_msg = await rate_limiter.can_execute(
request.plugin_id,
standardized_id,
plugin.safety.get("rate_limit", {}).get("max_per_hour", settings.max_tasks_per_hour),
client_id=client_id,
)
Expand All @@ -494,7 +496,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,
standardized_id,
effective_inputs,
safe_mode=safe_mode,
preset=request.preset,
Expand Down
Loading
Loading