souleyez 2.43.29__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 +22827 -10678
- 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-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/ai/recommender.py
CHANGED
|
@@ -59,12 +59,14 @@ If no hosts are discovered yet, suggest reconnaissance actions.
|
|
|
59
59
|
self.context_builder = ContextBuilder()
|
|
60
60
|
|
|
61
61
|
# For backward compatibility, also set ollama attribute if using OllamaProvider
|
|
62
|
-
if hasattr(self.provider,
|
|
62
|
+
if hasattr(self.provider, "service"):
|
|
63
63
|
self.ollama = self.provider.service
|
|
64
64
|
else:
|
|
65
65
|
self.ollama = None
|
|
66
66
|
|
|
67
|
-
def suggest_next_step(
|
|
67
|
+
def suggest_next_step(
|
|
68
|
+
self, engagement_id: int, target_host_ids: Optional[list] = None
|
|
69
|
+
) -> Optional[Dict[str, Any]]:
|
|
68
70
|
"""
|
|
69
71
|
Suggest the next best attack step for an engagement.
|
|
70
72
|
|
|
@@ -85,36 +87,40 @@ If no hosts are discovered yet, suggest reconnaissance actions.
|
|
|
85
87
|
if not self.provider or not self.provider.is_available():
|
|
86
88
|
logger.error("Cannot generate recommendation: AI provider not available")
|
|
87
89
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
"error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
|
|
91
|
+
"action": None,
|
|
92
|
+
"target": None,
|
|
93
|
+
"rationale": None,
|
|
94
|
+
"expected_outcome": None,
|
|
95
|
+
"risk_level": None,
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
# Build context from engagement data (with optional host filtering)
|
|
97
99
|
try:
|
|
98
|
-
context = self.context_builder.build_context(
|
|
100
|
+
context = self.context_builder.build_context(
|
|
101
|
+
engagement_id, target_host_ids=target_host_ids
|
|
102
|
+
)
|
|
99
103
|
except Exception as e:
|
|
100
104
|
logger.error(f"Failed to build context: {e}")
|
|
101
105
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
"error": f"Failed to load engagement data: {e}",
|
|
107
|
+
"action": None,
|
|
108
|
+
"target": None,
|
|
109
|
+
"rationale": None,
|
|
110
|
+
"expected_outcome": None,
|
|
111
|
+
"risk_level": None,
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
# Build prompt with state awareness
|
|
111
|
-
state_summary = self.context_builder.get_state_summary(
|
|
112
|
-
|
|
115
|
+
state_summary = self.context_builder.get_state_summary(
|
|
116
|
+
engagement_id, target_host_ids=target_host_ids
|
|
117
|
+
)
|
|
118
|
+
|
|
113
119
|
# Add targeting note if specific hosts selected
|
|
114
120
|
targeting_note = ""
|
|
115
121
|
if target_host_ids:
|
|
116
122
|
targeting_note = f"\nNOTE: Only target the {len(target_host_ids)} selected host(s) listed above. Do not suggest actions against other hosts.\n"
|
|
117
|
-
|
|
123
|
+
|
|
118
124
|
prompt = f"""You are an expert penetration tester analyzing an engagement and suggesting the next most promising attack step.
|
|
119
125
|
|
|
120
126
|
{state_summary}
|
|
@@ -158,47 +164,53 @@ Be specific and actionable. Focus on practical pentesting methodology.
|
|
|
158
164
|
Continue from current state - don't restart from scratch."""
|
|
159
165
|
|
|
160
166
|
# Generate recommendation
|
|
161
|
-
provider_type = getattr(self.provider,
|
|
162
|
-
logger.info(
|
|
167
|
+
provider_type = getattr(self.provider, "provider_type", "unknown")
|
|
168
|
+
logger.info(
|
|
169
|
+
f"Generating recommendation for engagement {engagement_id} using {provider_type}"
|
|
170
|
+
)
|
|
163
171
|
try:
|
|
164
|
-
response = self.provider.generate(
|
|
172
|
+
response = self.provider.generate(
|
|
173
|
+
prompt=prompt, max_tokens=2000, temperature=0.7
|
|
174
|
+
)
|
|
165
175
|
if not response:
|
|
166
176
|
logger.error("LLM returned empty response")
|
|
167
177
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
"error": "LLM generation failed (empty response). Check AI provider configuration.",
|
|
179
|
+
"action": None,
|
|
180
|
+
"target": None,
|
|
181
|
+
"rationale": None,
|
|
182
|
+
"expected_outcome": None,
|
|
183
|
+
"risk_level": None,
|
|
174
184
|
}
|
|
175
185
|
except Exception as e:
|
|
176
186
|
logger.error(f"LLM generation failed: {e}")
|
|
177
187
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
"error": f"LLM generation failed: {e}",
|
|
189
|
+
"action": None,
|
|
190
|
+
"target": None,
|
|
191
|
+
"rationale": None,
|
|
192
|
+
"expected_outcome": None,
|
|
193
|
+
"risk_level": None,
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
# Parse response
|
|
187
197
|
try:
|
|
188
198
|
recommendation = self._parse_response(response)
|
|
189
|
-
logger.info(
|
|
199
|
+
logger.info(
|
|
200
|
+
f"Generated recommendation: {recommendation.get('action', 'unknown')}"
|
|
201
|
+
)
|
|
190
202
|
return recommendation
|
|
191
203
|
except Exception as e:
|
|
192
204
|
logger.error(f"Failed to parse LLM response: {e}")
|
|
193
205
|
logger.debug(f"Raw response: {response}")
|
|
194
206
|
return {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
"error": f"Failed to parse LLM response: {e}",
|
|
208
|
+
"action": None,
|
|
209
|
+
"target": None,
|
|
210
|
+
"rationale": None,
|
|
211
|
+
"expected_outcome": None,
|
|
212
|
+
"risk_level": None,
|
|
213
|
+
"raw_response": response,
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
def _parse_response(self, response: str) -> Dict[str, Any]:
|
|
@@ -222,25 +234,37 @@ Continue from current state - don't restart from scratch."""
|
|
|
222
234
|
ValueError: If response format is invalid
|
|
223
235
|
"""
|
|
224
236
|
# Extract fields using regex
|
|
225
|
-
action_match = re.search(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
237
|
+
action_match = re.search(
|
|
238
|
+
r"ACTION:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
|
|
239
|
+
)
|
|
240
|
+
target_match = re.search(
|
|
241
|
+
r"TARGET:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
|
|
242
|
+
)
|
|
243
|
+
rationale_match = re.search(
|
|
244
|
+
r"RATIONALE:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
|
|
245
|
+
)
|
|
246
|
+
expected_match = re.search(
|
|
247
|
+
r"EXPECTED:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
|
|
248
|
+
)
|
|
249
|
+
risk_match = re.search(
|
|
250
|
+
r"RISK:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
|
|
251
|
+
)
|
|
230
252
|
|
|
231
253
|
# Validate all fields present
|
|
232
|
-
if not all(
|
|
254
|
+
if not all(
|
|
255
|
+
[action_match, target_match, rationale_match, expected_match, risk_match]
|
|
256
|
+
):
|
|
233
257
|
missing = []
|
|
234
258
|
if not action_match:
|
|
235
|
-
missing.append(
|
|
259
|
+
missing.append("ACTION")
|
|
236
260
|
if not target_match:
|
|
237
|
-
missing.append(
|
|
261
|
+
missing.append("TARGET")
|
|
238
262
|
if not rationale_match:
|
|
239
|
-
missing.append(
|
|
263
|
+
missing.append("RATIONALE")
|
|
240
264
|
if not expected_match:
|
|
241
|
-
missing.append(
|
|
265
|
+
missing.append("EXPECTED")
|
|
242
266
|
if not risk_match:
|
|
243
|
-
missing.append(
|
|
267
|
+
missing.append("RISK")
|
|
244
268
|
raise ValueError(f"Missing required fields: {', '.join(missing)}")
|
|
245
269
|
|
|
246
270
|
# Extract and clean values
|
|
@@ -248,33 +272,35 @@ Continue from current state - don't restart from scratch."""
|
|
|
248
272
|
target = target_match.group(1).strip()
|
|
249
273
|
rationale = rationale_match.group(1).strip()
|
|
250
274
|
expected = expected_match.group(1).strip()
|
|
251
|
-
|
|
275
|
+
|
|
252
276
|
# Extract risk level (grab only first word, handle multi-line responses)
|
|
253
277
|
risk_text = risk_match.group(1).strip()
|
|
254
|
-
risk = risk_text.split()[0].lower() if risk_text else
|
|
278
|
+
risk = risk_text.split()[0].lower() if risk_text else "medium"
|
|
255
279
|
|
|
256
280
|
# Validate risk level
|
|
257
|
-
if risk not in [
|
|
281
|
+
if risk not in ["low", "medium", "high"]:
|
|
258
282
|
logger.warning(f"Invalid risk level '{risk}', defaulting to 'medium'")
|
|
259
|
-
risk =
|
|
283
|
+
risk = "medium"
|
|
260
284
|
|
|
261
285
|
return {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
"error": None,
|
|
287
|
+
"action": action,
|
|
288
|
+
"target": target,
|
|
289
|
+
"rationale": rationale,
|
|
290
|
+
"expected_outcome": expected,
|
|
291
|
+
"risk_level": risk,
|
|
268
292
|
}
|
|
269
293
|
|
|
270
|
-
def generate_chain(
|
|
294
|
+
def generate_chain(
|
|
295
|
+
self, engagement_id: int, num_steps: int = 5
|
|
296
|
+
) -> Optional[Dict[str, Any]]:
|
|
271
297
|
"""
|
|
272
298
|
Generate a multi-step attack chain.
|
|
273
|
-
|
|
299
|
+
|
|
274
300
|
Args:
|
|
275
301
|
engagement_id: ID of engagement to analyze
|
|
276
302
|
num_steps: Number of steps to generate
|
|
277
|
-
|
|
303
|
+
|
|
278
304
|
Returns:
|
|
279
305
|
dict: Chain with list of steps, or error dict if failed
|
|
280
306
|
"""
|
|
@@ -282,8 +308,8 @@ Continue from current state - don't restart from scratch."""
|
|
|
282
308
|
if not self.provider or not self.provider.is_available():
|
|
283
309
|
logger.error("Cannot generate chain: AI provider not available")
|
|
284
310
|
return {
|
|
285
|
-
|
|
286
|
-
|
|
311
|
+
"error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
|
|
312
|
+
"steps": [],
|
|
287
313
|
}
|
|
288
314
|
|
|
289
315
|
# Build context from engagement data
|
|
@@ -291,62 +317,61 @@ Continue from current state - don't restart from scratch."""
|
|
|
291
317
|
context = self.context_builder.build_context(engagement_id)
|
|
292
318
|
except Exception as e:
|
|
293
319
|
logger.error(f"Failed to build context: {e}")
|
|
294
|
-
return {
|
|
295
|
-
'error': f'Failed to load engagement data: {e}',
|
|
296
|
-
'steps': []
|
|
297
|
-
}
|
|
320
|
+
return {"error": f"Failed to load engagement data: {e}", "steps": []}
|
|
298
321
|
|
|
299
322
|
# Create multi-step prompt
|
|
300
323
|
prompt = self._build_chain_prompt(context, num_steps, engagement_id)
|
|
301
324
|
|
|
302
325
|
# Get AI response
|
|
303
|
-
provider_type = getattr(self.provider,
|
|
304
|
-
logger.info(
|
|
326
|
+
provider_type = getattr(self.provider, "provider_type", "unknown")
|
|
327
|
+
logger.info(
|
|
328
|
+
f"Generating {num_steps}-step chain for engagement {engagement_id} using {provider_type}"
|
|
329
|
+
)
|
|
305
330
|
try:
|
|
306
|
-
response = self.provider.generate(
|
|
331
|
+
response = self.provider.generate(
|
|
332
|
+
prompt=prompt, max_tokens=3000, temperature=0.7
|
|
333
|
+
)
|
|
307
334
|
if not response:
|
|
308
335
|
logger.error("LLM returned empty response")
|
|
309
|
-
return {
|
|
310
|
-
'error': 'LLM generation failed (empty response)',
|
|
311
|
-
'steps': []
|
|
312
|
-
}
|
|
336
|
+
return {"error": "LLM generation failed (empty response)", "steps": []}
|
|
313
337
|
except Exception as e:
|
|
314
338
|
logger.error(f"LLM generation failed: {e}")
|
|
315
|
-
return {
|
|
316
|
-
'error': f'LLM generation failed: {e}',
|
|
317
|
-
'steps': []
|
|
318
|
-
}
|
|
339
|
+
return {"error": f"LLM generation failed: {e}", "steps": []}
|
|
319
340
|
|
|
320
341
|
# Parse multi-step response
|
|
321
342
|
try:
|
|
322
343
|
steps = self._parse_chain_response(response)
|
|
323
344
|
logger.info(f"Generated {len(steps)}-step attack chain")
|
|
324
|
-
|
|
345
|
+
|
|
325
346
|
# DEBUG: Log if we got empty steps
|
|
326
347
|
if not steps:
|
|
327
|
-
logger.warning(
|
|
328
|
-
|
|
348
|
+
logger.warning(
|
|
349
|
+
f"Parsed 0 steps from response. Raw response:\n{response}"
|
|
350
|
+
)
|
|
351
|
+
|
|
329
352
|
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
"error": None,
|
|
354
|
+
"engagement_id": engagement_id,
|
|
355
|
+
"num_steps": len(steps),
|
|
356
|
+
"steps": steps,
|
|
334
357
|
}
|
|
335
358
|
except Exception as e:
|
|
336
359
|
logger.error(f"Failed to parse chain response: {e}")
|
|
337
360
|
logger.debug(f"Raw response: {response}")
|
|
338
361
|
return {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
362
|
+
"error": f"Failed to parse chain response: {e}",
|
|
363
|
+
"steps": [],
|
|
364
|
+
"raw_response": response,
|
|
342
365
|
}
|
|
343
366
|
|
|
344
|
-
def _build_chain_prompt(
|
|
367
|
+
def _build_chain_prompt(
|
|
368
|
+
self, context: str, num_steps: int, engagement_id: int
|
|
369
|
+
) -> str:
|
|
345
370
|
"""Build prompt for multi-step attack chain with state awareness."""
|
|
346
|
-
|
|
371
|
+
|
|
347
372
|
# Get current state summary
|
|
348
373
|
state_summary = self.context_builder.get_state_summary(engagement_id)
|
|
349
|
-
|
|
374
|
+
|
|
350
375
|
return f"""You are an expert penetration tester. Analyze this engagement and create a {num_steps}-step attack chain.
|
|
351
376
|
|
|
352
377
|
{state_summary}
|
|
@@ -388,19 +413,21 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
388
413
|
logger.debug(f"Parsing chain response: {response[:200]}...")
|
|
389
414
|
|
|
390
415
|
# Split by "STEP N:" pattern (handle both with and without leading newline)
|
|
391
|
-
step_blocks = re.split(r
|
|
392
|
-
|
|
416
|
+
step_blocks = re.split(r"STEP \d+:", response)
|
|
417
|
+
|
|
393
418
|
logger.debug(f"Split into {len(step_blocks)} blocks")
|
|
394
419
|
|
|
395
|
-
for i, block in enumerate(
|
|
420
|
+
for i, block in enumerate(
|
|
421
|
+
step_blocks[1:], 1
|
|
422
|
+
): # Skip first block (before first STEP)
|
|
396
423
|
step = {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
424
|
+
"step_number": i,
|
|
425
|
+
"action": self._extract_field(block, "ACTION"),
|
|
426
|
+
"target": self._extract_field(block, "TARGET"),
|
|
427
|
+
"rationale": self._extract_field(block, "RATIONALE"),
|
|
428
|
+
"expected": self._extract_field(block, "EXPECTED"),
|
|
429
|
+
"risk": self._extract_risk_field(block),
|
|
430
|
+
"dependencies": self._extract_field(block, "DEPENDENCIES"),
|
|
404
431
|
}
|
|
405
432
|
steps.append(step)
|
|
406
433
|
|
|
@@ -408,7 +435,7 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
408
435
|
|
|
409
436
|
def _extract_field(self, text: str, field_name: str) -> str:
|
|
410
437
|
"""Extract a field value from text block."""
|
|
411
|
-
pattern = rf
|
|
438
|
+
pattern = rf"{field_name}:\s*(.+?)(?=\n[A-Z]+:|$)"
|
|
412
439
|
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
|
413
440
|
if match:
|
|
414
441
|
return match.group(1).strip()
|
|
@@ -416,17 +443,23 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
416
443
|
|
|
417
444
|
def _extract_risk_field(self, text: str) -> str:
|
|
418
445
|
"""Extract and validate risk field (handles multi-line responses)."""
|
|
419
|
-
risk_text = self._extract_field(text,
|
|
420
|
-
risk =
|
|
421
|
-
|
|
446
|
+
risk_text = self._extract_field(text, "RISK")
|
|
447
|
+
risk = (
|
|
448
|
+
risk_text.split()[0].upper()
|
|
449
|
+
if risk_text and risk_text != "Unknown"
|
|
450
|
+
else "MEDIUM"
|
|
451
|
+
)
|
|
452
|
+
|
|
422
453
|
# Validate risk level
|
|
423
|
-
if risk not in [
|
|
454
|
+
if risk not in ["LOW", "MEDIUM", "HIGH"]:
|
|
424
455
|
logger.warning(f"Invalid risk level '{risk}', defaulting to 'MEDIUM'")
|
|
425
|
-
risk =
|
|
426
|
-
|
|
456
|
+
risk = "MEDIUM"
|
|
457
|
+
|
|
427
458
|
return risk
|
|
428
459
|
|
|
429
|
-
def suggest_multiple_paths(
|
|
460
|
+
def suggest_multiple_paths(
|
|
461
|
+
self, engagement_id: int, num_paths: int = 3
|
|
462
|
+
) -> Dict[str, Any]:
|
|
430
463
|
"""
|
|
431
464
|
Generate multiple alternative attack paths and rank them.
|
|
432
465
|
|
|
@@ -444,8 +477,8 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
444
477
|
if not self.provider or not self.provider.is_available():
|
|
445
478
|
logger.error("Cannot generate paths: AI provider not available")
|
|
446
479
|
return {
|
|
447
|
-
|
|
448
|
-
|
|
480
|
+
"error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
|
|
481
|
+
"paths": [],
|
|
449
482
|
}
|
|
450
483
|
|
|
451
484
|
# Build context
|
|
@@ -453,31 +486,29 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
453
486
|
context = self.context_builder.build_context(engagement_id)
|
|
454
487
|
except Exception as e:
|
|
455
488
|
logger.error(f"Failed to build context: {e}")
|
|
456
|
-
return {
|
|
457
|
-
'error': f'Failed to load engagement data: {e}',
|
|
458
|
-
'paths': []
|
|
459
|
-
}
|
|
489
|
+
return {"error": f"Failed to load engagement data: {e}", "paths": []}
|
|
460
490
|
|
|
461
491
|
# Build multi-path prompt
|
|
462
492
|
prompt = self._build_multi_path_prompt(context, num_paths)
|
|
463
493
|
|
|
464
494
|
# Generate recommendations
|
|
465
|
-
provider_type = getattr(self.provider,
|
|
466
|
-
logger.info(
|
|
495
|
+
provider_type = getattr(self.provider, "provider_type", "unknown")
|
|
496
|
+
logger.info(
|
|
497
|
+
f"Generating {num_paths} alternative paths for engagement {engagement_id} using {provider_type}"
|
|
498
|
+
)
|
|
467
499
|
try:
|
|
468
|
-
response = self.provider.generate(
|
|
500
|
+
response = self.provider.generate(
|
|
501
|
+
prompt=prompt, max_tokens=2500, temperature=0.7
|
|
502
|
+
)
|
|
469
503
|
if not response:
|
|
470
504
|
logger.error("LLM returned empty response")
|
|
471
505
|
return {
|
|
472
|
-
|
|
473
|
-
|
|
506
|
+
"error": "AI generation failed. Check provider configuration.",
|
|
507
|
+
"paths": [],
|
|
474
508
|
}
|
|
475
509
|
except Exception as e:
|
|
476
510
|
logger.error(f"LLM generation failed: {e}")
|
|
477
|
-
return {
|
|
478
|
-
'error': f'LLM generation failed: {e}',
|
|
479
|
-
'paths': []
|
|
480
|
-
}
|
|
511
|
+
return {"error": f"LLM generation failed: {e}", "paths": []}
|
|
481
512
|
|
|
482
513
|
# Parse response
|
|
483
514
|
try:
|
|
@@ -486,6 +517,7 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
486
517
|
|
|
487
518
|
# Score and rank paths
|
|
488
519
|
from .path_scorer import PathScorer
|
|
520
|
+
|
|
489
521
|
scorer = PathScorer()
|
|
490
522
|
|
|
491
523
|
# Get engagement data for scoring
|
|
@@ -493,16 +525,16 @@ Be specific and actionable. Think like a professional pentester. Continue from c
|
|
|
493
525
|
ranked_paths = scorer.rank_paths(paths, engagement_data)
|
|
494
526
|
|
|
495
527
|
return {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
528
|
+
"error": None,
|
|
529
|
+
"engagement_id": engagement_id,
|
|
530
|
+
"paths": ranked_paths,
|
|
499
531
|
}
|
|
500
532
|
except Exception as e:
|
|
501
533
|
logger.error(f"Failed to parse/score paths: {e}")
|
|
502
534
|
return {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
535
|
+
"error": f"Failed to process paths: {e}",
|
|
536
|
+
"paths": [],
|
|
537
|
+
"raw_response": response,
|
|
506
538
|
}
|
|
507
539
|
|
|
508
540
|
def _build_multi_path_prompt(self, context: str, num_paths: int) -> str:
|
|
@@ -537,15 +569,15 @@ Be specific and actionable. Think like a professional pentester considering mult
|
|
|
537
569
|
paths = []
|
|
538
570
|
|
|
539
571
|
# Split by "PATH N:" pattern
|
|
540
|
-
path_blocks = re.split(r
|
|
572
|
+
path_blocks = re.split(r"PATH \d+:", response)
|
|
541
573
|
|
|
542
574
|
for i, block in enumerate(path_blocks[1:], 1): # Skip first block
|
|
543
575
|
path = {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
576
|
+
"action": self._extract_field(block, "ACTION"),
|
|
577
|
+
"target": self._extract_field(block, "TARGET"),
|
|
578
|
+
"rationale": self._extract_field(block, "RATIONALE"),
|
|
579
|
+
"expected": self._extract_field(block, "EXPECTED"),
|
|
580
|
+
"risk_level": self._extract_risk_field(block),
|
|
549
581
|
}
|
|
550
582
|
paths.append(path)
|
|
551
583
|
|
|
@@ -564,8 +596,8 @@ Be specific and actionable. Think like a professional pentester considering mult
|
|
|
564
596
|
fm = FindingsManager()
|
|
565
597
|
|
|
566
598
|
return {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
599
|
+
"engagement": em.get_by_id(engagement_id),
|
|
600
|
+
"hosts": hm.list_hosts(engagement_id),
|
|
601
|
+
"credentials": cm.list_credentials(engagement_id),
|
|
602
|
+
"findings": fm.list_findings(engagement_id),
|
|
571
603
|
}
|