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.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +22827 -10678
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/storage/hosts.py CHANGED
@@ -24,7 +24,7 @@ class HostManager:
24
24
  Returns:
25
25
  host_id
26
26
  """
27
- ip = host_data.get('ip')
27
+ ip = host_data.get("ip")
28
28
  if not ip:
29
29
  raise ValueError("Host must have an IP address")
30
30
 
@@ -34,53 +34,58 @@ class HostManager:
34
34
  # Check if host already exists
35
35
  existing = self.db.execute_one(
36
36
  "SELECT id, scope_status FROM hosts WHERE engagement_id = ? AND ip_address = ?",
37
- (engagement_id, ip)
37
+ (engagement_id, ip),
38
38
  )
39
39
 
40
40
  if existing:
41
41
  # Update existing host
42
- host_id = existing['id']
42
+ host_id = existing["id"]
43
43
  # Only update fields that have values (don't overwrite with NULL)
44
44
  update_data = {}
45
45
 
46
46
  # Always update status
47
- update_data['status'] = host_data.get('status', 'up')
47
+ update_data["status"] = host_data.get("status", "up")
48
48
 
49
49
  # Update scope_status if it was unknown and we now have a determination
50
- if existing.get('scope_status') == 'unknown' and scope_status != 'unknown':
51
- update_data['scope_status'] = scope_status
50
+ if existing.get("scope_status") == "unknown" and scope_status != "unknown":
51
+ update_data["scope_status"] = scope_status
52
52
 
53
53
  # Only update these fields if they have values
54
- if host_data.get('hostname'):
55
- update_data['hostname'] = host_data['hostname']
56
- if host_data.get('domain'):
57
- update_data['domain'] = host_data['domain']
58
- if host_data.get('os'):
59
- update_data['os_name'] = host_data['os']
60
- if host_data.get('mac_address'):
61
- update_data['mac_address'] = host_data['mac_address']
62
- if host_data.get('os_accuracy') is not None:
63
- update_data['os_accuracy'] = host_data['os_accuracy']
54
+ if host_data.get("hostname"):
55
+ update_data["hostname"] = host_data["hostname"]
56
+ if host_data.get("domain"):
57
+ update_data["domain"] = host_data["domain"]
58
+ if host_data.get("os"):
59
+ update_data["os_name"] = host_data["os"]
60
+ if host_data.get("mac_address"):
61
+ update_data["mac_address"] = host_data["mac_address"]
62
+ if host_data.get("os_accuracy") is not None:
63
+ update_data["os_accuracy"] = host_data["os_accuracy"]
64
64
 
65
65
  if update_data:
66
- updates = ', '.join([f"{k} = ?" for k in update_data.keys()])
66
+ updates = ", ".join([f"{k} = ?" for k in update_data.keys()])
67
67
  values = list(update_data.values()) + [host_id]
68
- self.db.execute(f"UPDATE hosts SET {updates} WHERE id = ?", tuple(values))
68
+ self.db.execute(
69
+ f"UPDATE hosts SET {updates} WHERE id = ?", tuple(values)
70
+ )
69
71
 
70
72
  return host_id
71
73
  else:
72
74
  # Insert new host
73
- host_id = self.db.insert('hosts', {
74
- 'engagement_id': engagement_id,
75
- 'ip_address': ip,
76
- 'hostname': host_data.get('hostname'),
77
- 'domain': host_data.get('domain'),
78
- 'os_name': host_data.get('os'),
79
- 'mac_address': host_data.get('mac_address'),
80
- 'os_accuracy': host_data.get('os_accuracy'),
81
- 'status': host_data.get('status', 'up'),
82
- 'scope_status': scope_status
83
- })
75
+ host_id = self.db.insert(
76
+ "hosts",
77
+ {
78
+ "engagement_id": engagement_id,
79
+ "ip_address": ip,
80
+ "hostname": host_data.get("hostname"),
81
+ "domain": host_data.get("domain"),
82
+ "os_name": host_data.get("os"),
83
+ "mac_address": host_data.get("mac_address"),
84
+ "os_accuracy": host_data.get("os_accuracy"),
85
+ "status": host_data.get("status", "up"),
86
+ "scope_status": scope_status,
87
+ },
88
+ )
84
89
 
85
90
  return host_id
86
91
 
@@ -97,14 +102,15 @@ class HostManager:
97
102
  """
98
103
  try:
99
104
  from souleyez.security.scope_validator import ScopeValidator
105
+
100
106
  validator = ScopeValidator(engagement_id)
101
107
  if validator.has_scope_defined():
102
108
  result = validator.validate_ip(ip)
103
- return 'in_scope' if result.is_in_scope else 'out_of_scope'
104
- return 'unknown' # No scope defined
109
+ return "in_scope" if result.is_in_scope else "out_of_scope"
110
+ return "unknown" # No scope defined
105
111
  except Exception as e:
106
112
  logger.warning(f"Failed to determine scope status for {ip}: {e}")
107
- return 'unknown'
113
+ return "unknown"
108
114
 
109
115
  def update_scope_status(self, host_id: int, scope_status: str) -> bool:
110
116
  """
@@ -117,14 +123,16 @@ class HostManager:
117
123
  Returns:
118
124
  True if successful
119
125
  """
120
- valid_statuses = ['in_scope', 'out_of_scope', 'unknown']
126
+ valid_statuses = ["in_scope", "out_of_scope", "unknown"]
121
127
  if scope_status not in valid_statuses:
122
- raise ValueError(f"Invalid scope_status: {scope_status}. Must be one of: {valid_statuses}")
128
+ raise ValueError(
129
+ f"Invalid scope_status: {scope_status}. Must be one of: {valid_statuses}"
130
+ )
123
131
 
124
132
  try:
125
133
  self.db.execute(
126
134
  "UPDATE hosts SET scope_status = ? WHERE id = ?",
127
- (scope_status, host_id)
135
+ (scope_status, host_id),
128
136
  )
129
137
  return True
130
138
  except Exception:
@@ -148,23 +156,26 @@ class HostManager:
148
156
  out_of_scope = 0
149
157
 
150
158
  for host in hosts:
151
- ip = host.get('ip') or host.get('ip_address')
159
+ ip = host.get("ip") or host.get("ip_address")
152
160
  new_status = self._determine_scope_status(engagement_id, ip)
153
- if new_status != host.get('scope_status'):
154
- self.update_scope_status(host['id'], new_status)
161
+ if new_status != host.get("scope_status"):
162
+ self.update_scope_status(host["id"], new_status)
155
163
  updated += 1
156
164
 
157
- if new_status == 'in_scope':
165
+ if new_status == "in_scope":
158
166
  in_scope += 1
159
- elif new_status == 'out_of_scope':
167
+ elif new_status == "out_of_scope":
160
168
  out_of_scope += 1
161
169
 
162
- return {'updated': updated, 'in_scope': in_scope, 'out_of_scope': out_of_scope}
170
+ return {"updated": updated, "in_scope": in_scope, "out_of_scope": out_of_scope}
163
171
 
164
172
  def add_service(self, host_id: int, service_data: Dict[str, Any]) -> int:
165
173
  """
166
174
  Add or update a service for a host.
167
175
 
176
+ Uses atomic upsert (INSERT ... ON CONFLICT DO UPDATE) to handle
177
+ duplicate services properly without race conditions.
178
+
168
179
  Args:
169
180
  host_id: Host ID
170
181
  service_data: Service data (port, protocol, state, service, version)
@@ -172,49 +183,57 @@ class HostManager:
172
183
  Returns:
173
184
  service_id
174
185
  """
175
- port = service_data.get('port')
176
- protocol = service_data.get('protocol', 'tcp')
186
+ port = service_data.get("port")
187
+ protocol = service_data.get("protocol", "tcp")
177
188
 
178
189
  if not port:
179
190
  raise ValueError("Service must have a port")
180
191
 
181
- # Check if service already exists
182
- existing = self.db.execute_one(
183
- "SELECT id FROM services WHERE host_id = ? AND port = ? AND protocol = ?",
184
- (host_id, port, protocol)
185
- )
186
-
187
- if existing:
188
- # Update existing service
189
- service_id = existing['id']
190
- update_data = {
191
- 'state': service_data.get('state', 'open'),
192
- 'service_name': service_data.get('service') or 'unknown',
193
- 'service_version': service_data.get('version'),
194
- 'service_product': service_data.get('product')
195
- }
192
+ state = service_data.get("state", "open")
193
+ service_name = service_data.get("service") or "unknown"
194
+ service_version = service_data.get("version")
195
+ service_product = service_data.get("product")
196
196
 
197
- updates = ', '.join([f"{k} = ?" for k in update_data.keys()])
198
- values = list(update_data.values()) + [service_id]
199
-
200
- self.db.execute(f"UPDATE services SET {updates} WHERE id = ?", tuple(values))
201
-
202
- return service_id
203
- else:
204
- # Insert new service
205
- service_id = self.db.insert('services', {
206
- 'host_id': host_id,
207
- 'port': port,
208
- 'protocol': protocol,
209
- 'state': service_data.get('state', 'open'),
210
- 'service_name': service_data.get('service') or 'unknown',
211
- 'service_version': service_data.get('version'),
212
- 'service_product': service_data.get('product')
213
- })
214
-
215
- return service_id
197
+ # Use atomic upsert - INSERT with ON CONFLICT UPDATE
198
+ # This handles duplicates properly without race conditions
199
+ conn = self.db.get_connection()
200
+ cursor = conn.cursor()
201
+ try:
202
+ cursor.execute(
203
+ """
204
+ INSERT INTO services (host_id, port, protocol, state, service_name, service_version, service_product)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?)
206
+ ON CONFLICT(host_id, port, protocol) DO UPDATE SET
207
+ state = excluded.state,
208
+ service_name = excluded.service_name,
209
+ service_version = COALESCE(excluded.service_version, service_version),
210
+ service_product = COALESCE(excluded.service_product, service_product)
211
+ """,
212
+ (
213
+ host_id,
214
+ port,
215
+ protocol,
216
+ state,
217
+ service_name,
218
+ service_version,
219
+ service_product,
220
+ ),
221
+ )
222
+ conn.commit()
223
+
224
+ # Get the service_id (either newly inserted or existing)
225
+ result = cursor.execute(
226
+ "SELECT id FROM services WHERE host_id = ? AND port = ? AND protocol = ?",
227
+ (host_id, port, protocol),
228
+ ).fetchone()
229
+ return result[0] if result else 0
230
+ except Exception as e:
231
+ conn.rollback()
232
+ raise e
216
233
 
217
- def import_nmap_results(self, engagement_id: int, parsed_data: Dict[str, Any]) -> Dict[str, int]:
234
+ def import_nmap_results(
235
+ self, engagement_id: int, parsed_data: Dict[str, Any]
236
+ ) -> Dict[str, int]:
218
237
  """
219
238
  Import parsed nmap results into the database.
220
239
 
@@ -228,57 +247,49 @@ class HostManager:
228
247
  hosts_added = 0
229
248
  services_added = 0
230
249
 
231
- for host_data in parsed_data.get('hosts', []):
250
+ for host_data in parsed_data.get("hosts", []):
232
251
  # Add/update host
233
252
  host_id = self.add_or_update_host(engagement_id, host_data)
234
253
 
235
254
  # Only count live hosts
236
- if host_data.get('status') == 'up':
255
+ if host_data.get("status") == "up":
237
256
  hosts_added += 1
238
257
 
239
258
  # Add services
240
- for service_data in host_data.get('services', []):
259
+ for service_data in host_data.get("services", []):
241
260
  self.add_service(host_id, service_data)
242
261
  services_added += 1
243
262
 
244
- return {
245
- 'hosts_added': hosts_added,
246
- 'services_added': services_added
247
- }
263
+ return {"hosts_added": hosts_added, "services_added": services_added}
248
264
 
249
265
  def list_hosts(self, engagement_id: int, limit: int = None) -> List[Dict[str, Any]]:
250
266
  """List all hosts in engagement with optional limit."""
251
267
  query = "SELECT * FROM hosts WHERE engagement_id = ? ORDER BY ip_address"
252
268
  params = [engagement_id]
253
-
269
+
254
270
  if limit:
255
271
  query += " LIMIT ?"
256
272
  params.append(limit)
257
-
273
+
258
274
  hosts = self.db.execute(query, tuple(params))
259
-
275
+
260
276
  # Normalize column names for compatibility (ip_address -> ip)
261
277
  return [
262
- {**host, 'ip': host.get('ip_address') or host.get('ip')}
263
- for host in hosts
278
+ {**host, "ip": host.get("ip_address") or host.get("ip")} for host in hosts
264
279
  ]
265
280
 
266
281
  def get_host(self, host_id: int) -> Optional[Dict[str, Any]]:
267
282
  """Get a single host by ID."""
268
- host = self.db.execute_one(
269
- "SELECT * FROM hosts WHERE id = ?",
270
- (host_id,)
271
- )
283
+ host = self.db.execute_one("SELECT * FROM hosts WHERE id = ?", (host_id,))
272
284
  if host:
273
285
  # Normalize column names for compatibility (ip_address -> ip)
274
- host['ip'] = host.get('ip_address') or host.get('ip')
286
+ host["ip"] = host.get("ip_address") or host.get("ip")
275
287
  return host
276
288
 
277
289
  def get_host_services(self, host_id: int) -> List[Dict[str, Any]]:
278
290
  """Get all services for a host."""
279
291
  return self.db.execute(
280
- "SELECT * FROM services WHERE host_id = ? ORDER BY port",
281
- (host_id,)
292
+ "SELECT * FROM services WHERE host_id = ? ORDER BY port", (host_id,)
282
293
  )
283
294
 
284
295
  def get_all_services(
@@ -288,7 +299,7 @@ class HostManager:
288
299
  port_min: int = None,
289
300
  port_max: int = None,
290
301
  protocol: str = None,
291
- sort_by: str = 'port'
302
+ sort_by: str = "port",
292
303
  ) -> List[Dict[str, Any]]:
293
304
  """
294
305
  Get all services across all hosts in engagement with optional filters.
@@ -332,9 +343,9 @@ class HostManager:
332
343
  params.append(protocol)
333
344
 
334
345
  # Add sorting
335
- if sort_by == 'service':
346
+ if sort_by == "service":
336
347
  query += " ORDER BY s.service_name, s.port"
337
- elif sort_by == 'protocol':
348
+ elif sort_by == "protocol":
338
349
  query += " ORDER BY s.protocol, s.port"
339
350
  else: # default to port
340
351
  query += " ORDER BY s.port"
@@ -345,7 +356,7 @@ class HostManager:
345
356
  """Get host by IP address."""
346
357
  return self.db.execute_one(
347
358
  "SELECT * FROM hosts WHERE engagement_id = ? AND ip_address = ?",
348
- (engagement_id, ip)
359
+ (engagement_id, ip),
349
360
  )
350
361
 
351
362
  def search_hosts(
@@ -354,7 +365,7 @@ class HostManager:
354
365
  search: str = None,
355
366
  os_name: str = None,
356
367
  status: str = None,
357
- tags: str = None
368
+ tags: str = None,
358
369
  ) -> List[Dict[str, Any]]:
359
370
  """
360
371
  Search and filter hosts.
@@ -409,17 +420,19 @@ class HostManager:
409
420
  if not host:
410
421
  return False
411
422
 
412
- current_tags = host.get('tags', '') or ''
413
- tag_list = [t.strip() for t in current_tags.split(',') if t.strip()]
423
+ current_tags = host.get("tags", "") or ""
424
+ tag_list = [t.strip() for t in current_tags.split(",") if t.strip()]
414
425
 
415
426
  # Add tag if not already present
416
427
  if tag not in tag_list:
417
428
  tag_list.append(tag)
418
429
 
419
- new_tags = ', '.join(tag_list)
430
+ new_tags = ", ".join(tag_list)
420
431
 
421
432
  try:
422
- self.db.execute("UPDATE hosts SET tags = ? WHERE id = ?", (new_tags, host_id))
433
+ self.db.execute(
434
+ "UPDATE hosts SET tags = ? WHERE id = ?", (new_tags, host_id)
435
+ )
423
436
  return True
424
437
  except Exception:
425
438
  return False
@@ -439,17 +452,19 @@ class HostManager:
439
452
  if not host:
440
453
  return False
441
454
 
442
- current_tags = host.get('tags', '') or ''
443
- tag_list = [t.strip() for t in current_tags.split(',') if t.strip()]
455
+ current_tags = host.get("tags", "") or ""
456
+ tag_list = [t.strip() for t in current_tags.split(",") if t.strip()]
444
457
 
445
458
  # Remove tag if present
446
459
  if tag in tag_list:
447
460
  tag_list.remove(tag)
448
461
 
449
- new_tags = ', '.join(tag_list)
462
+ new_tags = ", ".join(tag_list)
450
463
 
451
464
  try:
452
- self.db.execute("UPDATE hosts SET tags = ? WHERE id = ?", (new_tags, host_id))
465
+ self.db.execute(
466
+ "UPDATE hosts SET tags = ? WHERE id = ?", (new_tags, host_id)
467
+ )
453
468
  return True
454
469
  except Exception:
455
470
  return False
@@ -473,91 +488,103 @@ class HostManager:
473
488
 
474
489
  def get_all_tags(self, engagement_id: int) -> List[str]:
475
490
  """Get list of all unique tags used in engagement."""
476
- hosts = self.db.execute("SELECT tags FROM hosts WHERE engagement_id = ?", (engagement_id,))
491
+ hosts = self.db.execute(
492
+ "SELECT tags FROM hosts WHERE engagement_id = ?", (engagement_id,)
493
+ )
477
494
 
478
495
  all_tags = set()
479
496
  for host in hosts:
480
- tags_str = host.get('tags', '') or ''
497
+ tags_str = host.get("tags", "") or ""
481
498
  if tags_str:
482
- tags = [t.strip() for t in tags_str.split(',') if t.strip()]
499
+ tags = [t.strip() for t in tags_str.split(",") if t.strip()]
483
500
  all_tags.update(tags)
484
501
 
485
502
  return sorted(list(all_tags))
486
503
 
487
- def update_host_status(self, host_id: int, status: str = None, access_level: str = None, notes: str = None) -> bool:
504
+ def update_host_status(
505
+ self,
506
+ host_id: int,
507
+ status: str = None,
508
+ access_level: str = None,
509
+ notes: str = None,
510
+ ) -> bool:
488
511
  """
489
512
  Update host status and access level.
490
-
513
+
491
514
  Args:
492
515
  host_id: Host ID
493
516
  status: Host status (active/compromised/offline)
494
517
  access_level: Access level (none/user/admin/root)
495
518
  notes: Optional notes
496
-
519
+
497
520
  Returns:
498
521
  bool: True if successful
499
522
  """
500
523
  updates = []
501
524
  params = []
502
-
525
+
503
526
  if status:
504
527
  updates.append("status = ?")
505
528
  params.append(status)
506
-
529
+
507
530
  if access_level:
508
531
  updates.append("access_level = ?")
509
532
  params.append(access_level)
510
-
533
+
511
534
  if notes is not None:
512
535
  updates.append("notes = ?")
513
536
  params.append(notes)
514
-
537
+
515
538
  if not updates:
516
539
  return False
517
-
540
+
518
541
  params.append(host_id)
519
542
  query = f"UPDATE hosts SET {', '.join(updates)} WHERE id = ?"
520
-
543
+
521
544
  try:
522
545
  self.db.execute(query, tuple(params))
523
546
  return True
524
547
  except Exception:
525
548
  return False
526
-
549
+
527
550
  def update_hostname(self, host_id: int, hostname: str) -> bool:
528
551
  """
529
552
  Update hostname for a host.
530
-
553
+
531
554
  Args:
532
555
  host_id: Host ID
533
556
  hostname: New hostname
534
-
557
+
535
558
  Returns:
536
559
  bool: True if successful
537
560
  """
538
561
  try:
539
- self.db.execute("UPDATE hosts SET hostname = ? WHERE id = ?", (hostname, host_id))
562
+ self.db.execute(
563
+ "UPDATE hosts SET hostname = ? WHERE id = ?", (hostname, host_id)
564
+ )
540
565
  return True
541
566
  except Exception:
542
567
  return False
543
-
568
+
544
569
  def update_os(self, host_id: int, os_name: str) -> bool:
545
570
  """
546
571
  Update OS name for a host.
547
-
572
+
548
573
  Args:
549
574
  host_id: Host ID
550
575
  os_name: New OS name
551
-
576
+
552
577
  Returns:
553
578
  bool: True if successful
554
579
  """
555
580
  try:
556
- self.db.execute("UPDATE hosts SET os_name = ? WHERE id = ?", (os_name, host_id))
581
+ self.db.execute(
582
+ "UPDATE hosts SET os_name = ? WHERE id = ?", (os_name, host_id)
583
+ )
557
584
  return True
558
585
  except Exception:
559
586
  return False
560
-
587
+
561
588
  def delete_host(self, host_id: int) -> bool:
562
589
  """
563
590
  Delete a host and all associated data (services, findings, etc.).
@@ -574,6 +601,7 @@ class HostManager:
574
601
  # Check permission
575
602
  from souleyez.auth import get_current_user
576
603
  from souleyez.auth.permissions import Permission, PermissionChecker
604
+
577
605
  user = get_current_user()
578
606
  if user:
579
607
  checker = PermissionChecker(user.role, user.tier)
@@ -583,25 +611,29 @@ class HostManager:
583
611
  try:
584
612
  # Delete all associated data in correct order (respecting foreign keys)
585
613
  # Note: Foreign keys are not enabled by default in SQLite, so we handle explicitly
586
-
614
+
587
615
  # 1. Delete services (has FK to hosts with CASCADE)
588
616
  self.db.execute("DELETE FROM services WHERE host_id = ?", (host_id,))
589
-
617
+
590
618
  # 2. Delete web paths (has FK to hosts with CASCADE)
591
619
  self.db.execute("DELETE FROM web_paths WHERE host_id = ?", (host_id,))
592
-
620
+
593
621
  # 3. Delete SMB shares (has FK to hosts with CASCADE)
594
622
  self.db.execute("DELETE FROM smb_shares WHERE host_id = ?", (host_id,))
595
-
623
+
596
624
  # 4. Update findings to set host_id to NULL (has FK with SET NULL)
597
- self.db.execute("UPDATE findings SET host_id = NULL WHERE host_id = ?", (host_id,))
598
-
625
+ self.db.execute(
626
+ "UPDATE findings SET host_id = NULL WHERE host_id = ?", (host_id,)
627
+ )
628
+
599
629
  # 5. Update credentials to set host_id to NULL (has FK but no ON DELETE clause)
600
- self.db.execute("UPDATE credentials SET host_id = NULL WHERE host_id = ?", (host_id,))
601
-
630
+ self.db.execute(
631
+ "UPDATE credentials SET host_id = NULL WHERE host_id = ?", (host_id,)
632
+ )
633
+
602
634
  # 6. Finally delete the host
603
635
  self.db.execute("DELETE FROM hosts WHERE id = ?", (host_id,))
604
-
636
+
605
637
  logger.info(f"Deleted host {host_id} and all associated data")
606
638
  return True
607
639
  except Exception as e:
@@ -611,10 +643,10 @@ class HostManager:
611
643
  def get_host_vulnerability_count(self, host_id: int) -> int:
612
644
  """
613
645
  Get count of vulnerabilities (findings) for a specific host.
614
-
646
+
615
647
  Args:
616
648
  host_id: Host ID
617
-
649
+
618
650
  Returns:
619
651
  Count of vulnerability findings for this host
620
652
  """
@@ -623,6 +655,6 @@ class HostManager:
623
655
  WHERE host_id = ?
624
656
  AND finding_type IN ('vulnerability', 'sql_injection', 'xss', 'file_inclusion',
625
657
  'web_vulnerability', 'sql_injection_exploitation')""",
626
- (host_id,)
658
+ (host_id,),
627
659
  )
628
- return result['count'] if result else 0
660
+ return result["count"] if result else 0