souleyez 2.43.29__py3-none-any.whl → 3.0.0__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 +9564 -2881
- 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 +564 -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 +409 -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 +417 -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 +913 -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 +219 -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 +237 -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 +23034 -10679
- 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.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -20,12 +20,14 @@ logger = get_logger(__name__)
|
|
|
20
20
|
|
|
21
21
|
class ScopeViolationError(Exception):
|
|
22
22
|
"""Raised when a target is out of scope and enforcement is 'block'."""
|
|
23
|
+
|
|
23
24
|
pass
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
27
28
|
class ScopeValidationResult:
|
|
28
29
|
"""Result of scope validation check."""
|
|
30
|
+
|
|
29
31
|
is_in_scope: bool
|
|
30
32
|
matched_entry: Optional[Dict[str, Any]]
|
|
31
33
|
reason: str
|
|
@@ -71,15 +73,15 @@ class ScopeValidator:
|
|
|
71
73
|
FROM engagement_scope
|
|
72
74
|
WHERE engagement_id = ?
|
|
73
75
|
ORDER BY is_excluded ASC, scope_type ASC""",
|
|
74
|
-
(self.engagement_id,)
|
|
76
|
+
(self.engagement_id,),
|
|
75
77
|
)
|
|
76
78
|
self._scope_cache = entries
|
|
77
79
|
return entries
|
|
78
80
|
except Exception as e:
|
|
79
|
-
logger.warning(
|
|
80
|
-
"
|
|
81
|
-
"error": str(e)
|
|
82
|
-
|
|
81
|
+
logger.warning(
|
|
82
|
+
"Failed to get scope entries",
|
|
83
|
+
extra={"engagement_id": self.engagement_id, "error": str(e)},
|
|
84
|
+
)
|
|
83
85
|
return []
|
|
84
86
|
|
|
85
87
|
def has_scope_defined(self) -> bool:
|
|
@@ -91,7 +93,7 @@ class ScopeValidator:
|
|
|
91
93
|
"""
|
|
92
94
|
entries = self.get_scope_entries()
|
|
93
95
|
# Only count inclusion entries (not exclusions)
|
|
94
|
-
inclusions = [e for e in entries if not e.get(
|
|
96
|
+
inclusions = [e for e in entries if not e.get("is_excluded")]
|
|
95
97
|
return len(inclusions) > 0
|
|
96
98
|
|
|
97
99
|
def get_enforcement_mode(self) -> str:
|
|
@@ -107,17 +109,17 @@ class ScopeValidator:
|
|
|
107
109
|
try:
|
|
108
110
|
result = self.db.execute_one(
|
|
109
111
|
"SELECT scope_enforcement FROM engagements WHERE id = ?",
|
|
110
|
-
(self.engagement_id,)
|
|
112
|
+
(self.engagement_id,),
|
|
111
113
|
)
|
|
112
|
-
mode = result.get(
|
|
113
|
-
self._enforcement_cache = mode or
|
|
114
|
+
mode = result.get("scope_enforcement", "off") if result else "off"
|
|
115
|
+
self._enforcement_cache = mode or "off"
|
|
114
116
|
return self._enforcement_cache
|
|
115
117
|
except Exception as e:
|
|
116
|
-
logger.warning(
|
|
117
|
-
"
|
|
118
|
-
"error": str(e)
|
|
119
|
-
|
|
120
|
-
return
|
|
118
|
+
logger.warning(
|
|
119
|
+
"Failed to get enforcement mode",
|
|
120
|
+
extra={"engagement_id": self.engagement_id, "error": str(e)},
|
|
121
|
+
)
|
|
122
|
+
return "off"
|
|
121
123
|
|
|
122
124
|
def validate_target(self, target: str) -> ScopeValidationResult:
|
|
123
125
|
"""
|
|
@@ -128,6 +130,7 @@ class ScopeValidator:
|
|
|
128
130
|
- IP addresses
|
|
129
131
|
- CIDR ranges
|
|
130
132
|
- Hostnames/domains
|
|
133
|
+
- Space-separated multiple targets (validates each, all must be in scope)
|
|
131
134
|
|
|
132
135
|
Args:
|
|
133
136
|
target: The target to validate (IP, URL, hostname, etc.)
|
|
@@ -140,18 +143,48 @@ class ScopeValidator:
|
|
|
140
143
|
is_in_scope=False,
|
|
141
144
|
matched_entry=None,
|
|
142
145
|
reason="Empty target",
|
|
143
|
-
scope_type=None
|
|
146
|
+
scope_type=None,
|
|
144
147
|
)
|
|
145
148
|
|
|
146
149
|
target = target.strip()
|
|
147
150
|
|
|
151
|
+
# Handle space-separated multiple targets
|
|
152
|
+
# Check if this looks like multiple targets (space-separated, not a URL with spaces)
|
|
153
|
+
if " " in target and not target.startswith(("http://", "https://")):
|
|
154
|
+
targets = target.split()
|
|
155
|
+
# Validate each target - all must be in scope
|
|
156
|
+
for t in targets:
|
|
157
|
+
result = self._validate_single_target(t)
|
|
158
|
+
if not result.is_in_scope:
|
|
159
|
+
return result # Return first out-of-scope result
|
|
160
|
+
# All targets are in scope
|
|
161
|
+
return ScopeValidationResult(
|
|
162
|
+
is_in_scope=True,
|
|
163
|
+
matched_entry=None,
|
|
164
|
+
reason=f"All {len(targets)} targets are in scope",
|
|
165
|
+
scope_type=None,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return self._validate_single_target(target)
|
|
169
|
+
|
|
170
|
+
def _validate_single_target(self, target: str) -> ScopeValidationResult:
|
|
171
|
+
"""
|
|
172
|
+
Validate a single target against the engagement scope.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
target: Single target to validate (IP, URL, hostname, etc.)
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
ScopeValidationResult with validation outcome
|
|
179
|
+
"""
|
|
180
|
+
|
|
148
181
|
# If no scope defined, everything is in scope (permissive default)
|
|
149
182
|
if not self.has_scope_defined():
|
|
150
183
|
return ScopeValidationResult(
|
|
151
184
|
is_in_scope=True,
|
|
152
185
|
matched_entry=None,
|
|
153
186
|
reason="No scope defined (permissive)",
|
|
154
|
-
scope_type=None
|
|
187
|
+
scope_type=None,
|
|
155
188
|
)
|
|
156
189
|
|
|
157
190
|
# Determine target type and extract relevant part
|
|
@@ -162,26 +195,26 @@ class ScopeValidator:
|
|
|
162
195
|
|
|
163
196
|
# First check exclusions (deny rules take precedence)
|
|
164
197
|
for entry in entries:
|
|
165
|
-
if not entry.get(
|
|
198
|
+
if not entry.get("is_excluded"):
|
|
166
199
|
continue
|
|
167
200
|
if self._matches_entry(normalized, target_type, entry):
|
|
168
201
|
return ScopeValidationResult(
|
|
169
202
|
is_in_scope=False,
|
|
170
203
|
matched_entry=entry,
|
|
171
204
|
reason=f"Explicitly excluded by scope entry: {entry['value']}",
|
|
172
|
-
scope_type=entry[
|
|
205
|
+
scope_type=entry["scope_type"],
|
|
173
206
|
)
|
|
174
207
|
|
|
175
208
|
# Then check inclusions
|
|
176
209
|
for entry in entries:
|
|
177
|
-
if entry.get(
|
|
210
|
+
if entry.get("is_excluded"):
|
|
178
211
|
continue
|
|
179
212
|
if self._matches_entry(normalized, target_type, entry):
|
|
180
213
|
return ScopeValidationResult(
|
|
181
214
|
is_in_scope=True,
|
|
182
215
|
matched_entry=entry,
|
|
183
216
|
reason=f"Matched scope entry: {entry['value']}",
|
|
184
|
-
scope_type=entry[
|
|
217
|
+
scope_type=entry["scope_type"],
|
|
185
218
|
)
|
|
186
219
|
|
|
187
220
|
# No match found - out of scope
|
|
@@ -189,7 +222,7 @@ class ScopeValidator:
|
|
|
189
222
|
is_in_scope=False,
|
|
190
223
|
matched_entry=None,
|
|
191
224
|
reason=f"Target '{target}' does not match any scope entry",
|
|
192
|
-
scope_type=None
|
|
225
|
+
scope_type=None,
|
|
193
226
|
)
|
|
194
227
|
|
|
195
228
|
def validate_ip(self, ip: str) -> ScopeValidationResult:
|
|
@@ -228,8 +261,13 @@ class ScopeValidator:
|
|
|
228
261
|
"""
|
|
229
262
|
return self.validate_target(url)
|
|
230
263
|
|
|
231
|
-
def log_validation(
|
|
232
|
-
|
|
264
|
+
def log_validation(
|
|
265
|
+
self,
|
|
266
|
+
target: str,
|
|
267
|
+
result: ScopeValidationResult,
|
|
268
|
+
action: str,
|
|
269
|
+
job_id: int = None,
|
|
270
|
+
) -> None:
|
|
233
271
|
"""
|
|
234
272
|
Log a validation result to the audit trail.
|
|
235
273
|
|
|
@@ -241,31 +279,40 @@ class ScopeValidator:
|
|
|
241
279
|
"""
|
|
242
280
|
try:
|
|
243
281
|
from souleyez.auth import get_current_user
|
|
282
|
+
|
|
244
283
|
user = get_current_user()
|
|
245
284
|
user_id = user.id if user else None
|
|
246
285
|
except Exception:
|
|
247
286
|
user_id = None
|
|
248
287
|
|
|
249
|
-
validation_result =
|
|
288
|
+
validation_result = "in_scope" if result.is_in_scope else "out_of_scope"
|
|
250
289
|
if not self.has_scope_defined():
|
|
251
|
-
validation_result =
|
|
290
|
+
validation_result = "no_scope_defined"
|
|
252
291
|
|
|
253
292
|
try:
|
|
254
|
-
self.db.insert(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
self.db.insert(
|
|
294
|
+
"scope_validation_log",
|
|
295
|
+
{
|
|
296
|
+
"engagement_id": self.engagement_id,
|
|
297
|
+
"job_id": job_id,
|
|
298
|
+
"target": target,
|
|
299
|
+
"validation_result": validation_result,
|
|
300
|
+
"action_taken": action,
|
|
301
|
+
"matched_scope_id": (
|
|
302
|
+
result.matched_entry.get("id") if result.matched_entry else None
|
|
303
|
+
),
|
|
304
|
+
"user_id": user_id,
|
|
305
|
+
},
|
|
306
|
+
)
|
|
263
307
|
except Exception as e:
|
|
264
|
-
logger.warning(
|
|
265
|
-
"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Failed to log scope validation",
|
|
310
|
+
extra={
|
|
311
|
+
"engagement_id": self.engagement_id,
|
|
312
|
+
"target": target,
|
|
313
|
+
"error": str(e),
|
|
314
|
+
},
|
|
315
|
+
)
|
|
269
316
|
|
|
270
317
|
def _parse_target(self, target: str) -> tuple:
|
|
271
318
|
"""
|
|
@@ -276,35 +323,37 @@ class ScopeValidator:
|
|
|
276
323
|
target_type is one of: 'ip', 'cidr', 'domain', 'url'
|
|
277
324
|
"""
|
|
278
325
|
# Check if URL
|
|
279
|
-
if target.startswith((
|
|
326
|
+
if target.startswith(("http://", "https://")):
|
|
280
327
|
parsed = urlparse(target)
|
|
281
|
-
host = parsed.netloc.split(
|
|
328
|
+
host = parsed.netloc.split(":")[0] # Remove port
|
|
282
329
|
# Check if host part is IP
|
|
283
330
|
try:
|
|
284
331
|
ipaddress.ip_address(host)
|
|
285
|
-
return (
|
|
332
|
+
return ("ip", host)
|
|
286
333
|
except ValueError:
|
|
287
|
-
return (
|
|
334
|
+
return ("domain", host.lower())
|
|
288
335
|
|
|
289
336
|
# Check if IP address
|
|
290
337
|
try:
|
|
291
338
|
ipaddress.ip_address(target)
|
|
292
|
-
return (
|
|
339
|
+
return ("ip", target)
|
|
293
340
|
except ValueError:
|
|
294
341
|
pass
|
|
295
342
|
|
|
296
343
|
# Check if CIDR notation
|
|
297
|
-
if
|
|
344
|
+
if "/" in target:
|
|
298
345
|
try:
|
|
299
346
|
ipaddress.ip_network(target, strict=False)
|
|
300
|
-
return (
|
|
347
|
+
return ("cidr", target)
|
|
301
348
|
except ValueError:
|
|
302
349
|
pass
|
|
303
350
|
|
|
304
351
|
# Assume domain/hostname
|
|
305
|
-
return (
|
|
352
|
+
return ("domain", target.lower())
|
|
306
353
|
|
|
307
|
-
def _matches_entry(
|
|
354
|
+
def _matches_entry(
|
|
355
|
+
self, target: str, target_type: str, entry: Dict[str, Any]
|
|
356
|
+
) -> bool:
|
|
308
357
|
"""
|
|
309
358
|
Check if a target matches a scope entry.
|
|
310
359
|
|
|
@@ -316,46 +365,48 @@ class ScopeValidator:
|
|
|
316
365
|
Returns:
|
|
317
366
|
True if matches, False otherwise
|
|
318
367
|
"""
|
|
319
|
-
entry_type = entry[
|
|
320
|
-
entry_value = entry[
|
|
368
|
+
entry_type = entry["scope_type"]
|
|
369
|
+
entry_value = entry["value"]
|
|
321
370
|
|
|
322
371
|
# IP target
|
|
323
|
-
if target_type ==
|
|
324
|
-
if entry_type ==
|
|
372
|
+
if target_type == "ip":
|
|
373
|
+
if entry_type == "cidr":
|
|
325
374
|
return self._ip_in_cidr(target, entry_value)
|
|
326
|
-
elif entry_type ==
|
|
375
|
+
elif entry_type == "hostname":
|
|
327
376
|
# Exact IP match
|
|
328
377
|
return target == entry_value
|
|
329
|
-
elif entry_type ==
|
|
378
|
+
elif entry_type == "domain":
|
|
330
379
|
# IP doesn't match domain patterns
|
|
331
380
|
return False
|
|
332
|
-
elif entry_type ==
|
|
381
|
+
elif entry_type == "url":
|
|
333
382
|
# Extract host from URL entry
|
|
334
383
|
try:
|
|
335
384
|
parsed = urlparse(entry_value)
|
|
336
|
-
return target == parsed.netloc.split(
|
|
385
|
+
return target == parsed.netloc.split(":")[0]
|
|
337
386
|
except Exception:
|
|
338
387
|
return False
|
|
339
388
|
|
|
340
389
|
# CIDR target (less common - check containment)
|
|
341
|
-
elif target_type ==
|
|
342
|
-
if entry_type ==
|
|
390
|
+
elif target_type == "cidr":
|
|
391
|
+
if entry_type == "cidr":
|
|
343
392
|
return self._cidr_overlaps(target, entry_value)
|
|
344
393
|
return False
|
|
345
394
|
|
|
346
395
|
# Domain target
|
|
347
|
-
elif target_type ==
|
|
348
|
-
if entry_type ==
|
|
396
|
+
elif target_type == "domain":
|
|
397
|
+
if entry_type == "domain":
|
|
349
398
|
return self._domain_matches(target, entry_value)
|
|
350
|
-
elif entry_type ==
|
|
399
|
+
elif entry_type == "hostname":
|
|
351
400
|
# Exact hostname match
|
|
352
401
|
return target.lower() == entry_value.lower()
|
|
353
|
-
elif entry_type ==
|
|
402
|
+
elif entry_type == "url":
|
|
354
403
|
# Extract host from URL entry
|
|
355
404
|
try:
|
|
356
405
|
parsed = urlparse(entry_value)
|
|
357
|
-
entry_host = parsed.netloc.split(
|
|
358
|
-
return target == entry_host or self._domain_matches(
|
|
406
|
+
entry_host = parsed.netloc.split(":")[0].lower()
|
|
407
|
+
return target == entry_host or self._domain_matches(
|
|
408
|
+
target, entry_host
|
|
409
|
+
)
|
|
359
410
|
except Exception:
|
|
360
411
|
return False
|
|
361
412
|
return False
|
|
@@ -400,14 +451,14 @@ class ScopeValidator:
|
|
|
400
451
|
target = target.lower()
|
|
401
452
|
|
|
402
453
|
# Handle wildcard patterns
|
|
403
|
-
if pattern.startswith(
|
|
454
|
+
if pattern.startswith("*."):
|
|
404
455
|
# Remove the *. prefix for suffix matching
|
|
405
456
|
suffix = pattern[2:]
|
|
406
457
|
# Match exact suffix or .suffix
|
|
407
|
-
return target == suffix or target.endswith(
|
|
458
|
+
return target == suffix or target.endswith("." + suffix)
|
|
408
459
|
|
|
409
460
|
# Handle wildcards in other positions using fnmatch
|
|
410
|
-
if
|
|
461
|
+
if "*" in pattern or "?" in pattern:
|
|
411
462
|
return fnmatch.fnmatch(target, pattern)
|
|
412
463
|
|
|
413
464
|
# Exact match
|
|
@@ -428,8 +479,14 @@ class ScopeManager:
|
|
|
428
479
|
def __init__(self):
|
|
429
480
|
self.db = get_db()
|
|
430
481
|
|
|
431
|
-
def add_scope(
|
|
432
|
-
|
|
482
|
+
def add_scope(
|
|
483
|
+
self,
|
|
484
|
+
engagement_id: int,
|
|
485
|
+
scope_type: str,
|
|
486
|
+
value: str,
|
|
487
|
+
is_excluded: bool = False,
|
|
488
|
+
description: str = None,
|
|
489
|
+
) -> int:
|
|
433
490
|
"""
|
|
434
491
|
Add a scope entry for an engagement.
|
|
435
492
|
|
|
@@ -446,28 +503,34 @@ class ScopeManager:
|
|
|
446
503
|
Raises:
|
|
447
504
|
ValueError: If scope_type or value is invalid
|
|
448
505
|
"""
|
|
449
|
-
valid_types = [
|
|
506
|
+
valid_types = ["cidr", "domain", "url", "hostname"]
|
|
450
507
|
if scope_type not in valid_types:
|
|
451
|
-
raise ValueError(
|
|
508
|
+
raise ValueError(
|
|
509
|
+
f"Invalid scope_type: {scope_type}. Must be one of: {valid_types}"
|
|
510
|
+
)
|
|
452
511
|
|
|
453
512
|
# Validate the value based on type
|
|
454
513
|
self._validate_scope_value(scope_type, value)
|
|
455
514
|
|
|
456
515
|
try:
|
|
457
516
|
from souleyez.auth import get_current_user
|
|
517
|
+
|
|
458
518
|
user = get_current_user()
|
|
459
519
|
added_by = user.id if user else None
|
|
460
520
|
except Exception:
|
|
461
521
|
added_by = None
|
|
462
522
|
|
|
463
|
-
return self.db.insert(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
523
|
+
return self.db.insert(
|
|
524
|
+
"engagement_scope",
|
|
525
|
+
{
|
|
526
|
+
"engagement_id": engagement_id,
|
|
527
|
+
"scope_type": scope_type,
|
|
528
|
+
"value": value,
|
|
529
|
+
"is_excluded": is_excluded,
|
|
530
|
+
"description": description,
|
|
531
|
+
"added_by": added_by,
|
|
532
|
+
},
|
|
533
|
+
)
|
|
471
534
|
|
|
472
535
|
def remove_scope(self, scope_id: int) -> bool:
|
|
473
536
|
"""
|
|
@@ -480,16 +543,13 @@ class ScopeManager:
|
|
|
480
543
|
True if removed, False if not found
|
|
481
544
|
"""
|
|
482
545
|
try:
|
|
483
|
-
self.db.execute(
|
|
484
|
-
"DELETE FROM engagement_scope WHERE id = ?",
|
|
485
|
-
(scope_id,)
|
|
486
|
-
)
|
|
546
|
+
self.db.execute("DELETE FROM engagement_scope WHERE id = ?", (scope_id,))
|
|
487
547
|
return True
|
|
488
548
|
except Exception as e:
|
|
489
|
-
logger.warning(
|
|
490
|
-
"
|
|
491
|
-
"error": str(e)
|
|
492
|
-
|
|
549
|
+
logger.warning(
|
|
550
|
+
"Failed to remove scope entry",
|
|
551
|
+
extra={"scope_id": scope_id, "error": str(e)},
|
|
552
|
+
)
|
|
493
553
|
return False
|
|
494
554
|
|
|
495
555
|
def list_scope(self, engagement_id: int) -> List[Dict[str, Any]]:
|
|
@@ -507,7 +567,7 @@ class ScopeManager:
|
|
|
507
567
|
FROM engagement_scope
|
|
508
568
|
WHERE engagement_id = ?
|
|
509
569
|
ORDER BY is_excluded ASC, scope_type ASC, value ASC""",
|
|
510
|
-
(engagement_id,)
|
|
570
|
+
(engagement_id,),
|
|
511
571
|
)
|
|
512
572
|
|
|
513
573
|
def set_enforcement(self, engagement_id: int, mode: str) -> bool:
|
|
@@ -524,25 +584,28 @@ class ScopeManager:
|
|
|
524
584
|
Raises:
|
|
525
585
|
ValueError: If mode is invalid
|
|
526
586
|
"""
|
|
527
|
-
valid_modes = [
|
|
587
|
+
valid_modes = ["off", "warn", "block"]
|
|
528
588
|
if mode not in valid_modes:
|
|
529
|
-
raise ValueError(
|
|
589
|
+
raise ValueError(
|
|
590
|
+
f"Invalid enforcement mode: {mode}. Must be one of: {valid_modes}"
|
|
591
|
+
)
|
|
530
592
|
|
|
531
593
|
try:
|
|
532
594
|
self.db.execute(
|
|
533
595
|
"UPDATE engagements SET scope_enforcement = ? WHERE id = ?",
|
|
534
|
-
(mode, engagement_id)
|
|
596
|
+
(mode, engagement_id),
|
|
535
597
|
)
|
|
536
598
|
return True
|
|
537
599
|
except Exception as e:
|
|
538
|
-
logger.warning(
|
|
539
|
-
"
|
|
540
|
-
"mode": mode,
|
|
541
|
-
|
|
542
|
-
})
|
|
600
|
+
logger.warning(
|
|
601
|
+
"Failed to set enforcement mode",
|
|
602
|
+
extra={"engagement_id": engagement_id, "mode": mode, "error": str(e)},
|
|
603
|
+
)
|
|
543
604
|
return False
|
|
544
605
|
|
|
545
|
-
def get_validation_log(
|
|
606
|
+
def get_validation_log(
|
|
607
|
+
self, engagement_id: int, limit: int = 100
|
|
608
|
+
) -> List[Dict[str, Any]]:
|
|
546
609
|
"""
|
|
547
610
|
Get scope validation log for an engagement.
|
|
548
611
|
|
|
@@ -560,7 +623,7 @@ class ScopeManager:
|
|
|
560
623
|
WHERE engagement_id = ?
|
|
561
624
|
ORDER BY created_at DESC
|
|
562
625
|
LIMIT ?""",
|
|
563
|
-
(engagement_id, limit)
|
|
626
|
+
(engagement_id, limit),
|
|
564
627
|
)
|
|
565
628
|
|
|
566
629
|
def _validate_scope_value(self, scope_type: str, value: str) -> None:
|
|
@@ -575,13 +638,13 @@ class ScopeManager:
|
|
|
575
638
|
|
|
576
639
|
value = value.strip()
|
|
577
640
|
|
|
578
|
-
if scope_type ==
|
|
641
|
+
if scope_type == "cidr":
|
|
579
642
|
try:
|
|
580
643
|
ipaddress.ip_network(value, strict=False)
|
|
581
644
|
except ValueError:
|
|
582
645
|
raise ValueError(f"Invalid CIDR notation: {value}")
|
|
583
646
|
|
|
584
|
-
elif scope_type ==
|
|
647
|
+
elif scope_type == "hostname":
|
|
585
648
|
# Basic hostname validation (can be IP or hostname)
|
|
586
649
|
try:
|
|
587
650
|
ipaddress.ip_address(value)
|
|
@@ -589,23 +652,29 @@ class ScopeManager:
|
|
|
589
652
|
# Not an IP, validate as hostname
|
|
590
653
|
if len(value) > 253:
|
|
591
654
|
raise ValueError("Hostname too long (max 253 characters)")
|
|
592
|
-
if not re.match(
|
|
655
|
+
if not re.match(
|
|
656
|
+
r"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$",
|
|
657
|
+
value,
|
|
658
|
+
):
|
|
593
659
|
raise ValueError(f"Invalid hostname format: {value}")
|
|
594
660
|
|
|
595
|
-
elif scope_type ==
|
|
661
|
+
elif scope_type == "domain":
|
|
596
662
|
# Allow wildcards like *.example.com
|
|
597
|
-
if value.startswith(
|
|
663
|
+
if value.startswith("*."):
|
|
598
664
|
domain_part = value[2:]
|
|
599
665
|
else:
|
|
600
666
|
domain_part = value
|
|
601
667
|
|
|
602
668
|
if len(domain_part) > 253:
|
|
603
669
|
raise ValueError("Domain too long (max 253 characters)")
|
|
604
|
-
if not re.match(
|
|
670
|
+
if not re.match(
|
|
671
|
+
r"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$",
|
|
672
|
+
domain_part,
|
|
673
|
+
):
|
|
605
674
|
raise ValueError(f"Invalid domain format: {value}")
|
|
606
675
|
|
|
607
|
-
elif scope_type ==
|
|
608
|
-
if not value.startswith((
|
|
676
|
+
elif scope_type == "url":
|
|
677
|
+
if not value.startswith(("http://", "https://")):
|
|
609
678
|
raise ValueError("URL must start with http:// or https://")
|
|
610
679
|
try:
|
|
611
680
|
parsed = urlparse(value)
|