souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/security/validation.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Input validation and sanitization for security-critical operations.
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import ipaddress
|
|
7
|
+
import re
|
|
7
8
|
import shlex
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import List,
|
|
10
|
+
from typing import List, Optional, Union
|
|
11
|
+
|
|
10
12
|
from souleyez.log_config import get_logger
|
|
11
13
|
|
|
12
14
|
logger = get_logger(__name__)
|
|
@@ -14,21 +16,23 @@ logger = get_logger(__name__)
|
|
|
14
16
|
|
|
15
17
|
class ValidationError(Exception):
|
|
16
18
|
"""Raised when input validation fails."""
|
|
19
|
+
|
|
17
20
|
pass
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
# ===== IP ADDRESS VALIDATION =====
|
|
21
24
|
|
|
25
|
+
|
|
22
26
|
def validate_ip_address(ip: str) -> str:
|
|
23
27
|
"""
|
|
24
28
|
Validate and normalize an IP address.
|
|
25
|
-
|
|
29
|
+
|
|
26
30
|
Args:
|
|
27
31
|
ip: IP address string
|
|
28
|
-
|
|
32
|
+
|
|
29
33
|
Returns:
|
|
30
34
|
Normalized IP address string
|
|
31
|
-
|
|
35
|
+
|
|
32
36
|
Raises:
|
|
33
37
|
ValidationError: If IP is invalid
|
|
34
38
|
"""
|
|
@@ -37,23 +41,20 @@ def validate_ip_address(ip: str) -> str:
|
|
|
37
41
|
ip_obj = ipaddress.ip_address(ip)
|
|
38
42
|
return str(ip_obj)
|
|
39
43
|
except ValueError as e:
|
|
40
|
-
logger.warning("Invalid IP address", extra={
|
|
41
|
-
"input": ip,
|
|
42
|
-
"error": str(e)
|
|
43
|
-
})
|
|
44
|
+
logger.warning("Invalid IP address", extra={"input": ip, "error": str(e)})
|
|
44
45
|
raise ValidationError(f"Invalid IP address: {ip}")
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def validate_cidr(cidr: str) -> str:
|
|
48
49
|
"""
|
|
49
50
|
Validate and normalize a CIDR notation network.
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
Args:
|
|
52
53
|
cidr: CIDR string (e.g., "192.168.1.0/24")
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
Returns:
|
|
55
56
|
Normalized CIDR string
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
Raises:
|
|
58
59
|
ValidationError: If CIDR is invalid
|
|
59
60
|
"""
|
|
@@ -61,54 +62,52 @@ def validate_cidr(cidr: str) -> str:
|
|
|
61
62
|
network = ipaddress.ip_network(cidr, strict=False)
|
|
62
63
|
return str(network)
|
|
63
64
|
except ValueError as e:
|
|
64
|
-
logger.warning("Invalid CIDR notation", extra={
|
|
65
|
-
"input": cidr,
|
|
66
|
-
"error": str(e)
|
|
67
|
-
})
|
|
65
|
+
logger.warning("Invalid CIDR notation", extra={"input": cidr, "error": str(e)})
|
|
68
66
|
raise ValidationError(f"Invalid CIDR notation: {cidr}")
|
|
69
67
|
|
|
70
68
|
|
|
71
69
|
def validate_hostname(hostname: str) -> str:
|
|
72
70
|
"""
|
|
73
71
|
Validate hostname format.
|
|
74
|
-
|
|
72
|
+
|
|
75
73
|
Args:
|
|
76
74
|
hostname: Hostname string
|
|
77
|
-
|
|
75
|
+
|
|
78
76
|
Returns:
|
|
79
77
|
Validated hostname
|
|
80
|
-
|
|
78
|
+
|
|
81
79
|
Raises:
|
|
82
80
|
ValidationError: If hostname is invalid
|
|
83
81
|
"""
|
|
84
82
|
# RFC 1123 hostname rules
|
|
85
83
|
if not hostname or len(hostname) > 253:
|
|
86
84
|
raise ValidationError("Hostname must be 1-253 characters")
|
|
87
|
-
|
|
85
|
+
|
|
88
86
|
# Valid hostname regex
|
|
89
87
|
hostname_pattern = re.compile(
|
|
90
|
-
r
|
|
88
|
+
r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*$"
|
|
91
89
|
)
|
|
92
|
-
|
|
90
|
+
|
|
93
91
|
if not hostname_pattern.match(hostname):
|
|
94
92
|
logger.warning("Invalid hostname format", extra={"input": hostname})
|
|
95
93
|
raise ValidationError(f"Invalid hostname format: {hostname}")
|
|
96
|
-
|
|
94
|
+
|
|
97
95
|
return hostname
|
|
98
96
|
|
|
99
97
|
|
|
100
98
|
# ===== PORT VALIDATION =====
|
|
101
99
|
|
|
100
|
+
|
|
102
101
|
def validate_port(port: Union[int, str]) -> int:
|
|
103
102
|
"""
|
|
104
103
|
Validate port number.
|
|
105
|
-
|
|
104
|
+
|
|
106
105
|
Args:
|
|
107
106
|
port: Port number (1-65535)
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
Returns:
|
|
110
109
|
Validated port as integer
|
|
111
|
-
|
|
110
|
+
|
|
112
111
|
Raises:
|
|
113
112
|
ValidationError: If port is invalid
|
|
114
113
|
"""
|
|
@@ -124,28 +123,28 @@ def validate_port(port: Union[int, str]) -> int:
|
|
|
124
123
|
def validate_port_list(ports: str) -> str:
|
|
125
124
|
"""
|
|
126
125
|
Validate and sanitize port list for nmap.
|
|
127
|
-
|
|
126
|
+
|
|
128
127
|
Args:
|
|
129
128
|
ports: Port specification (e.g., "80,443", "1-1000", "80,443,8000-9000")
|
|
130
|
-
|
|
129
|
+
|
|
131
130
|
Returns:
|
|
132
131
|
Validated port string
|
|
133
|
-
|
|
132
|
+
|
|
134
133
|
Raises:
|
|
135
134
|
ValidationError: If port specification is invalid
|
|
136
135
|
"""
|
|
137
136
|
# Allow comma-separated ports and ranges
|
|
138
|
-
port_pattern = re.compile(r
|
|
139
|
-
|
|
137
|
+
port_pattern = re.compile(r"^[0-9,\-]+$")
|
|
138
|
+
|
|
140
139
|
if not port_pattern.match(ports):
|
|
141
140
|
raise ValidationError(f"Invalid port specification: {ports}")
|
|
142
|
-
|
|
141
|
+
|
|
143
142
|
# Validate each component
|
|
144
|
-
for part in ports.split(
|
|
145
|
-
if
|
|
143
|
+
for part in ports.split(","):
|
|
144
|
+
if "-" in part:
|
|
146
145
|
# Range
|
|
147
146
|
try:
|
|
148
|
-
start, end = part.split(
|
|
147
|
+
start, end = part.split("-")
|
|
149
148
|
start_port = validate_port(start)
|
|
150
149
|
end_port = validate_port(end)
|
|
151
150
|
if start_port > end_port:
|
|
@@ -155,25 +154,29 @@ def validate_port_list(ports: str) -> str:
|
|
|
155
154
|
else:
|
|
156
155
|
# Single port
|
|
157
156
|
validate_port(part)
|
|
158
|
-
|
|
157
|
+
|
|
159
158
|
return ports
|
|
160
159
|
|
|
161
160
|
|
|
162
161
|
# ===== PATH VALIDATION =====
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
|
|
164
|
+
def validate_file_path(
|
|
165
|
+
path: Union[str, Path],
|
|
166
|
+
must_exist: bool = False,
|
|
167
|
+
allowed_dirs: Optional[List[Path]] = None,
|
|
168
|
+
) -> Path:
|
|
166
169
|
"""
|
|
167
170
|
Validate and sanitize file path to prevent directory traversal.
|
|
168
|
-
|
|
171
|
+
|
|
169
172
|
Args:
|
|
170
173
|
path: File path to validate
|
|
171
174
|
must_exist: If True, path must exist
|
|
172
175
|
allowed_dirs: List of allowed parent directories (None = user's .souleyez dir only)
|
|
173
|
-
|
|
176
|
+
|
|
174
177
|
Returns:
|
|
175
178
|
Resolved absolute Path object
|
|
176
|
-
|
|
179
|
+
|
|
177
180
|
Raises:
|
|
178
181
|
ValidationError: If path is invalid or unsafe
|
|
179
182
|
"""
|
|
@@ -181,11 +184,11 @@ def validate_file_path(path: Union[str, Path], must_exist: bool = False,
|
|
|
181
184
|
path_obj = Path(path).expanduser().resolve()
|
|
182
185
|
except Exception as e:
|
|
183
186
|
raise ValidationError(f"Invalid path: {path} - {e}")
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
# Default to only allowing .souleyez directory
|
|
186
189
|
if allowed_dirs is None:
|
|
187
|
-
allowed_dirs = [Path.home() /
|
|
188
|
-
|
|
190
|
+
allowed_dirs = [Path.home() / ".souleyez"]
|
|
191
|
+
|
|
189
192
|
# Ensure resolved path is within allowed directories
|
|
190
193
|
is_allowed = False
|
|
191
194
|
for allowed_dir in allowed_dirs:
|
|
@@ -196,30 +199,34 @@ def validate_file_path(path: Union[str, Path], must_exist: bool = False,
|
|
|
196
199
|
break
|
|
197
200
|
except ValueError:
|
|
198
201
|
continue
|
|
199
|
-
|
|
202
|
+
|
|
200
203
|
if not is_allowed:
|
|
201
|
-
logger.warning(
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
logger.warning(
|
|
205
|
+
"Path traversal attempt blocked",
|
|
206
|
+
extra={
|
|
207
|
+
"requested_path": str(path),
|
|
208
|
+
"resolved_path": str(path_obj),
|
|
209
|
+
"allowed_dirs": [str(d) for d in allowed_dirs],
|
|
210
|
+
},
|
|
211
|
+
)
|
|
206
212
|
raise ValidationError(f"Path outside allowed directories: {path}")
|
|
207
|
-
|
|
213
|
+
|
|
208
214
|
if must_exist and not path_obj.exists():
|
|
209
215
|
raise ValidationError(f"Path does not exist: {path}")
|
|
210
|
-
|
|
216
|
+
|
|
211
217
|
return path_obj
|
|
212
218
|
|
|
213
219
|
|
|
214
220
|
# ===== COMMAND INJECTION PREVENTION =====
|
|
215
221
|
|
|
222
|
+
|
|
216
223
|
def sanitize_command_arg(arg: str) -> str:
|
|
217
224
|
"""
|
|
218
225
|
Sanitize a single command argument using shlex.
|
|
219
|
-
|
|
226
|
+
|
|
220
227
|
Args:
|
|
221
228
|
arg: Command argument to sanitize
|
|
222
|
-
|
|
229
|
+
|
|
223
230
|
Returns:
|
|
224
231
|
Safely quoted argument
|
|
225
232
|
"""
|
|
@@ -229,72 +236,73 @@ def sanitize_command_arg(arg: str) -> str:
|
|
|
229
236
|
def validate_nmap_args(args: List[str]) -> List[str]:
|
|
230
237
|
"""
|
|
231
238
|
Validate nmap arguments to prevent command injection.
|
|
232
|
-
|
|
239
|
+
|
|
233
240
|
Args:
|
|
234
241
|
args: List of nmap arguments
|
|
235
|
-
|
|
242
|
+
|
|
236
243
|
Returns:
|
|
237
244
|
Validated argument list
|
|
238
|
-
|
|
245
|
+
|
|
239
246
|
Raises:
|
|
240
247
|
ValidationError: If dangerous arguments detected
|
|
241
248
|
"""
|
|
242
249
|
# Dangerous patterns that could lead to command injection
|
|
243
250
|
# These patterns should only match within script arguments
|
|
244
251
|
dangerous_patterns = [
|
|
245
|
-
r
|
|
246
|
-
r
|
|
247
|
-
r
|
|
248
|
-
r
|
|
249
|
-
r
|
|
252
|
+
r"--script[=\s].*&&", # Command chaining in scripts
|
|
253
|
+
r"--script[=\s].*;", # Command separator in scripts
|
|
254
|
+
r"--script[=\s].*\|", # Pipe in scripts
|
|
255
|
+
r"--script[=\s].*`", # Command substitution in scripts
|
|
256
|
+
r"--script[=\s].*\$", # Variable expansion in scripts
|
|
250
257
|
]
|
|
251
|
-
|
|
258
|
+
|
|
252
259
|
# Blocked arguments that could be abused
|
|
253
260
|
blocked_args = [
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
"--interactive", # Interactive mode
|
|
262
|
+
"--iflist", # Could reveal sensitive info
|
|
256
263
|
]
|
|
257
|
-
|
|
264
|
+
|
|
258
265
|
validated_args = []
|
|
259
266
|
for arg in args:
|
|
260
267
|
# Check for dangerous patterns
|
|
261
268
|
for pattern in dangerous_patterns:
|
|
262
269
|
if re.search(pattern, arg):
|
|
263
|
-
logger.warning(
|
|
264
|
-
"argument"
|
|
265
|
-
"pattern": pattern
|
|
266
|
-
|
|
270
|
+
logger.warning(
|
|
271
|
+
"Blocked dangerous nmap argument",
|
|
272
|
+
extra={"argument": arg, "pattern": pattern},
|
|
273
|
+
)
|
|
267
274
|
raise ValidationError(f"Dangerous nmap argument blocked: {arg}")
|
|
268
|
-
|
|
275
|
+
|
|
269
276
|
# Check for blocked arguments
|
|
270
277
|
arg_lower = arg.lower()
|
|
271
278
|
for blocked in blocked_args:
|
|
272
279
|
if arg_lower.startswith(blocked):
|
|
273
280
|
logger.warning("Blocked nmap argument", extra={"argument": arg})
|
|
274
281
|
raise ValidationError(f"Blocked nmap argument: {arg}")
|
|
275
|
-
|
|
282
|
+
|
|
276
283
|
validated_args.append(arg)
|
|
277
|
-
|
|
284
|
+
|
|
278
285
|
return validated_args
|
|
279
286
|
|
|
280
287
|
|
|
281
288
|
# ===== SQL INJECTION PREVENTION =====
|
|
282
289
|
|
|
290
|
+
|
|
283
291
|
def validate_table_name(table: str) -> str:
|
|
284
292
|
"""
|
|
285
293
|
Validate table name to prevent SQL injection.
|
|
286
294
|
Only alphanumeric and underscore allowed.
|
|
287
|
-
|
|
295
|
+
|
|
288
296
|
Args:
|
|
289
297
|
table: Table name
|
|
290
|
-
|
|
298
|
+
|
|
291
299
|
Returns:
|
|
292
300
|
Validated table name
|
|
293
|
-
|
|
301
|
+
|
|
294
302
|
Raises:
|
|
295
303
|
ValidationError: If table name is invalid
|
|
296
304
|
"""
|
|
297
|
-
if not re.match(r
|
|
305
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", table):
|
|
298
306
|
raise ValidationError(f"Invalid table name: {table}")
|
|
299
307
|
return table
|
|
300
308
|
|
|
@@ -303,17 +311,17 @@ def validate_column_name(column: str) -> str:
|
|
|
303
311
|
"""
|
|
304
312
|
Validate column name to prevent SQL injection.
|
|
305
313
|
Only alphanumeric and underscore allowed.
|
|
306
|
-
|
|
314
|
+
|
|
307
315
|
Args:
|
|
308
316
|
column: Column name
|
|
309
|
-
|
|
317
|
+
|
|
310
318
|
Returns:
|
|
311
319
|
Validated column name
|
|
312
|
-
|
|
320
|
+
|
|
313
321
|
Raises:
|
|
314
322
|
ValidationError: If column name is invalid
|
|
315
323
|
"""
|
|
316
|
-
if not re.match(r
|
|
324
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", column):
|
|
317
325
|
raise ValidationError(f"Invalid column name: {column}")
|
|
318
326
|
return column
|
|
319
327
|
|
|
@@ -321,186 +329,195 @@ def validate_column_name(column: str) -> str:
|
|
|
321
329
|
def validate_severity(severity: str) -> str:
|
|
322
330
|
"""
|
|
323
331
|
Validate finding severity level.
|
|
324
|
-
|
|
332
|
+
|
|
325
333
|
Args:
|
|
326
334
|
severity: Severity string
|
|
327
|
-
|
|
335
|
+
|
|
328
336
|
Returns:
|
|
329
337
|
Validated severity (lowercase)
|
|
330
|
-
|
|
338
|
+
|
|
331
339
|
Raises:
|
|
332
340
|
ValidationError: If severity is invalid
|
|
333
341
|
"""
|
|
334
|
-
valid_severities = [
|
|
342
|
+
valid_severities = ["critical", "high", "medium", "low", "info"]
|
|
335
343
|
severity_lower = severity.lower()
|
|
336
|
-
|
|
344
|
+
|
|
337
345
|
if severity_lower not in valid_severities:
|
|
338
|
-
raise ValidationError(
|
|
339
|
-
|
|
346
|
+
raise ValidationError(
|
|
347
|
+
f"Invalid severity: {severity}. Must be one of: {valid_severities}"
|
|
348
|
+
)
|
|
349
|
+
|
|
340
350
|
return severity_lower
|
|
341
351
|
|
|
342
352
|
|
|
343
353
|
# ===== HTML/XSS PREVENTION =====
|
|
344
354
|
|
|
355
|
+
|
|
345
356
|
def escape_html(text: str) -> str:
|
|
346
357
|
"""
|
|
347
358
|
Escape HTML special characters to prevent XSS.
|
|
348
|
-
|
|
359
|
+
|
|
349
360
|
Args:
|
|
350
361
|
text: Text to escape
|
|
351
|
-
|
|
362
|
+
|
|
352
363
|
Returns:
|
|
353
364
|
HTML-escaped text
|
|
354
365
|
"""
|
|
355
366
|
if text is None:
|
|
356
|
-
return
|
|
357
|
-
|
|
367
|
+
return ""
|
|
368
|
+
|
|
358
369
|
escape_dict = {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'"':
|
|
363
|
-
"'":
|
|
364
|
-
|
|
370
|
+
"&": "&",
|
|
371
|
+
"<": "<",
|
|
372
|
+
">": ">",
|
|
373
|
+
'"': """,
|
|
374
|
+
"'": "'",
|
|
375
|
+
"/": "/",
|
|
365
376
|
}
|
|
366
|
-
|
|
367
|
-
return
|
|
377
|
+
|
|
378
|
+
return "".join(escape_dict.get(c, c) for c in str(text))
|
|
368
379
|
|
|
369
380
|
|
|
370
381
|
def sanitize_for_json(text: str) -> str:
|
|
371
382
|
"""
|
|
372
383
|
Sanitize text for safe inclusion in JSON.
|
|
373
|
-
|
|
384
|
+
|
|
374
385
|
Args:
|
|
375
386
|
text: Text to sanitize
|
|
376
|
-
|
|
387
|
+
|
|
377
388
|
Returns:
|
|
378
389
|
Sanitized text
|
|
379
390
|
"""
|
|
380
391
|
if text is None:
|
|
381
|
-
return
|
|
382
|
-
|
|
392
|
+
return ""
|
|
393
|
+
|
|
383
394
|
# Remove control characters
|
|
384
|
-
text = re.sub(r
|
|
395
|
+
text = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", str(text))
|
|
385
396
|
return text
|
|
386
397
|
|
|
387
398
|
|
|
388
399
|
# ===== ENGAGEMENT NAME VALIDATION =====
|
|
389
400
|
|
|
401
|
+
|
|
390
402
|
def validate_engagement_name(name: str) -> str:
|
|
391
403
|
"""
|
|
392
404
|
Validate engagement name.
|
|
393
|
-
|
|
405
|
+
|
|
394
406
|
Args:
|
|
395
407
|
name: Engagement name
|
|
396
|
-
|
|
408
|
+
|
|
397
409
|
Returns:
|
|
398
410
|
Validated name
|
|
399
|
-
|
|
411
|
+
|
|
400
412
|
Raises:
|
|
401
413
|
ValidationError: If name is invalid
|
|
402
414
|
"""
|
|
403
415
|
if not name or len(name) < 1:
|
|
404
416
|
raise ValidationError("Engagement name cannot be empty")
|
|
405
|
-
|
|
417
|
+
|
|
406
418
|
if len(name) > 255:
|
|
407
419
|
raise ValidationError("Engagement name too long (max 255 characters)")
|
|
408
|
-
|
|
420
|
+
|
|
409
421
|
# Prevent path traversal in names
|
|
410
|
-
if
|
|
422
|
+
if "/" in name or "\\" in name or ".." in name:
|
|
411
423
|
raise ValidationError("Engagement name cannot contain path separators")
|
|
412
|
-
|
|
424
|
+
|
|
413
425
|
return name.strip()
|
|
414
426
|
|
|
415
427
|
|
|
416
428
|
# ===== PLUGIN NAME VALIDATION =====
|
|
417
429
|
|
|
430
|
+
|
|
418
431
|
def validate_plugin_name(plugin: str) -> str:
|
|
419
432
|
"""
|
|
420
433
|
Validate plugin name to prevent code injection.
|
|
421
|
-
|
|
434
|
+
|
|
422
435
|
Args:
|
|
423
436
|
plugin: Plugin name
|
|
424
|
-
|
|
437
|
+
|
|
425
438
|
Returns:
|
|
426
439
|
Validated plugin name
|
|
427
|
-
|
|
440
|
+
|
|
428
441
|
Raises:
|
|
429
442
|
ValidationError: If plugin name is invalid
|
|
430
443
|
"""
|
|
431
444
|
# Only lowercase letters, numbers, underscores
|
|
432
|
-
if not re.match(r
|
|
445
|
+
if not re.match(r"^[a-z][a-z0-9_]*$", plugin):
|
|
433
446
|
raise ValidationError(f"Invalid plugin name: {plugin}")
|
|
434
|
-
|
|
447
|
+
|
|
435
448
|
if len(plugin) > 50:
|
|
436
449
|
raise ValidationError("Plugin name too long")
|
|
437
|
-
|
|
450
|
+
|
|
438
451
|
return plugin
|
|
439
452
|
|
|
440
453
|
|
|
441
454
|
# ===== PROTOCOL VALIDATION =====
|
|
442
455
|
|
|
456
|
+
|
|
443
457
|
def validate_protocol(protocol: str) -> str:
|
|
444
458
|
"""
|
|
445
459
|
Validate network protocol.
|
|
446
|
-
|
|
460
|
+
|
|
447
461
|
Args:
|
|
448
462
|
protocol: Protocol string (tcp, udp, etc.)
|
|
449
|
-
|
|
463
|
+
|
|
450
464
|
Returns:
|
|
451
465
|
Validated protocol (lowercase)
|
|
452
|
-
|
|
466
|
+
|
|
453
467
|
Raises:
|
|
454
468
|
ValidationError: If protocol is invalid
|
|
455
469
|
"""
|
|
456
|
-
valid_protocols = [
|
|
470
|
+
valid_protocols = ["tcp", "udp", "icmp", "sctp"]
|
|
457
471
|
protocol_lower = protocol.lower()
|
|
458
|
-
|
|
472
|
+
|
|
459
473
|
if protocol_lower not in valid_protocols:
|
|
460
|
-
raise ValidationError(
|
|
461
|
-
|
|
474
|
+
raise ValidationError(
|
|
475
|
+
f"Invalid protocol: {protocol}. Must be one of: {valid_protocols}"
|
|
476
|
+
)
|
|
477
|
+
|
|
462
478
|
return protocol_lower
|
|
463
479
|
|
|
464
480
|
|
|
465
481
|
# ===== UNIVERSAL TARGET VALIDATION =====
|
|
466
482
|
|
|
483
|
+
|
|
467
484
|
def validate_target(target: str) -> str:
|
|
468
485
|
"""
|
|
469
486
|
Validate a target which can be IP, CIDR, or hostname.
|
|
470
487
|
Tries each format in order and returns the first match.
|
|
471
|
-
|
|
488
|
+
|
|
472
489
|
Args:
|
|
473
490
|
target: Target string (IP, CIDR, or hostname)
|
|
474
|
-
|
|
491
|
+
|
|
475
492
|
Returns:
|
|
476
493
|
Validated target string
|
|
477
|
-
|
|
494
|
+
|
|
478
495
|
Raises:
|
|
479
496
|
ValidationError: If target is not valid in any format
|
|
480
497
|
"""
|
|
481
498
|
if not target or not target.strip():
|
|
482
499
|
raise ValidationError("Target cannot be empty")
|
|
483
|
-
|
|
500
|
+
|
|
484
501
|
target = target.strip()
|
|
485
|
-
|
|
502
|
+
|
|
486
503
|
# Try IP address
|
|
487
504
|
try:
|
|
488
505
|
return validate_ip_address(target)
|
|
489
506
|
except ValidationError:
|
|
490
507
|
pass
|
|
491
|
-
|
|
508
|
+
|
|
492
509
|
# Try CIDR notation
|
|
493
510
|
try:
|
|
494
511
|
return validate_cidr(target)
|
|
495
512
|
except ValidationError:
|
|
496
513
|
pass
|
|
497
|
-
|
|
514
|
+
|
|
498
515
|
# Try hostname
|
|
499
516
|
try:
|
|
500
517
|
return validate_hostname(target)
|
|
501
518
|
except ValidationError:
|
|
502
519
|
pass
|
|
503
|
-
|
|
520
|
+
|
|
504
521
|
# Nothing matched
|
|
505
522
|
raise ValidationError(
|
|
506
523
|
f"Invalid target: '{target}'. Must be IP address, CIDR notation, or valid hostname"
|
|
@@ -509,40 +526,97 @@ def validate_target(target: str) -> str:
|
|
|
509
526
|
|
|
510
527
|
# ===== URL VALIDATION =====
|
|
511
528
|
|
|
529
|
+
|
|
512
530
|
def validate_url(url: str) -> str:
|
|
513
531
|
"""
|
|
514
532
|
Validate URL format for web-based tools.
|
|
515
|
-
|
|
533
|
+
|
|
516
534
|
Args:
|
|
517
535
|
url: URL string
|
|
518
|
-
|
|
536
|
+
|
|
519
537
|
Returns:
|
|
520
538
|
Validated URL
|
|
521
|
-
|
|
539
|
+
|
|
522
540
|
Raises:
|
|
523
541
|
ValidationError: If URL is invalid
|
|
524
542
|
"""
|
|
525
543
|
if not url or not url.strip():
|
|
526
544
|
raise ValidationError("URL cannot be empty")
|
|
527
|
-
|
|
545
|
+
|
|
528
546
|
url = url.strip()
|
|
529
|
-
|
|
547
|
+
|
|
530
548
|
# Basic URL pattern (http/https only)
|
|
531
549
|
import re
|
|
550
|
+
|
|
532
551
|
url_pattern = re.compile(
|
|
533
|
-
r
|
|
534
|
-
r
|
|
535
|
-
r
|
|
536
|
-
r
|
|
537
|
-
r
|
|
538
|
-
r
|
|
552
|
+
r"^https?://" # http:// or https://
|
|
553
|
+
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|" # domain
|
|
554
|
+
r"localhost|" # localhost
|
|
555
|
+
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # or IP
|
|
556
|
+
r"(?::\d+)?" # optional port
|
|
557
|
+
r"(?:/?|[/?]\S+)$",
|
|
558
|
+
re.IGNORECASE, # optional path
|
|
539
559
|
)
|
|
540
|
-
|
|
560
|
+
|
|
541
561
|
if not url_pattern.match(url):
|
|
542
|
-
raise ValidationError(
|
|
543
|
-
|
|
562
|
+
raise ValidationError(
|
|
563
|
+
f"Invalid URL format: {url}. Must start with http:// or https://"
|
|
564
|
+
)
|
|
565
|
+
|
|
544
566
|
# Block dangerous protocols
|
|
545
|
-
if url.lower().startswith((
|
|
567
|
+
if url.lower().startswith(("file://", "ftp://", "javascript:", "data:")):
|
|
546
568
|
raise ValidationError(f"Dangerous URL protocol blocked: {url}")
|
|
547
|
-
|
|
569
|
+
|
|
548
570
|
return url
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def validate_target_or_url(target: str) -> str:
|
|
574
|
+
"""
|
|
575
|
+
Validate a target which can be IP, CIDR, hostname, or URL.
|
|
576
|
+
Tries each format in order and returns the first match.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
target: Target string (IP, CIDR, hostname, or URL)
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Validated target string
|
|
583
|
+
|
|
584
|
+
Raises:
|
|
585
|
+
ValidationError: If target is not valid in any format
|
|
586
|
+
"""
|
|
587
|
+
if not target or not target.strip():
|
|
588
|
+
raise ValidationError("Target cannot be empty")
|
|
589
|
+
|
|
590
|
+
target = target.strip()
|
|
591
|
+
|
|
592
|
+
# Try URL first (if it has a scheme)
|
|
593
|
+
if target.startswith(("http://", "https://")):
|
|
594
|
+
try:
|
|
595
|
+
return validate_url(target)
|
|
596
|
+
except ValidationError:
|
|
597
|
+
pass
|
|
598
|
+
|
|
599
|
+
# Try IP address
|
|
600
|
+
try:
|
|
601
|
+
return validate_ip_address(target)
|
|
602
|
+
except ValidationError:
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
# Try CIDR notation
|
|
606
|
+
try:
|
|
607
|
+
return validate_cidr(target)
|
|
608
|
+
except ValidationError:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
# Try hostname (more lenient pattern for domains)
|
|
612
|
+
# Allow single-part hostnames and domains
|
|
613
|
+
hostname_pattern = re.compile(
|
|
614
|
+
r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.?$"
|
|
615
|
+
)
|
|
616
|
+
if hostname_pattern.match(target) and len(target) <= 253:
|
|
617
|
+
return target
|
|
618
|
+
|
|
619
|
+
# Nothing matched
|
|
620
|
+
raise ValidationError(
|
|
621
|
+
f"Invalid target: '{target}'. Must be IP address, CIDR notation, hostname, or URL"
|
|
622
|
+
)
|