souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
AI-powered recommendation engine for deliverables.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from typing import Dict, List, Optional
|
|
5
6
|
from datetime import datetime, timedelta
|
|
6
7
|
from .database import get_db
|
|
@@ -11,29 +12,29 @@ from .timeline_tracker import TimelineTracker
|
|
|
11
12
|
|
|
12
13
|
class RecommendationEngine:
|
|
13
14
|
"""Generate smart recommendations for next actions and coverage gaps."""
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
def __init__(self):
|
|
16
17
|
self.db = get_db()
|
|
17
18
|
self.dm = DeliverableManager()
|
|
18
19
|
self.em = EvidenceManager()
|
|
19
20
|
self.tt = TimelineTracker()
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
def get_recommendations(self, engagement_id: int) -> Dict:
|
|
22
23
|
"""
|
|
23
24
|
Generate comprehensive recommendations for an engagement.
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
Returns:
|
|
26
27
|
Dict with recommendation categories
|
|
27
28
|
"""
|
|
28
29
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
"next_actions": self._get_next_actions(engagement_id),
|
|
31
|
+
"blockers": self._get_blocker_recommendations(engagement_id),
|
|
32
|
+
"quick_wins": self._get_quick_wins(engagement_id),
|
|
33
|
+
"coverage_gaps": self._get_coverage_gaps(engagement_id),
|
|
34
|
+
"at_risk": self._get_at_risk_deliverables(engagement_id),
|
|
35
|
+
"priority_boost": self._get_priority_boost_suggestions(engagement_id),
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
def _get_next_actions(self, engagement_id: int) -> List[Dict]:
|
|
38
39
|
"""
|
|
39
40
|
Recommend next deliverables to work on based on:
|
|
@@ -43,141 +44,152 @@ class RecommendationEngine:
|
|
|
43
44
|
- Time estimates
|
|
44
45
|
"""
|
|
45
46
|
deliverables = self.dm.list_deliverables(engagement_id)
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
# Filter to pending/in_progress only
|
|
48
|
-
actionable = [
|
|
49
|
-
|
|
49
|
+
actionable = [
|
|
50
|
+
d for d in deliverables if d["status"] in ["pending", "in_progress"]
|
|
51
|
+
]
|
|
52
|
+
|
|
50
53
|
recommendations = []
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
for d in actionable:
|
|
53
56
|
score = 0
|
|
54
57
|
reasons = []
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
# Priority scoring (critical = 100, high = 75, medium = 50, low = 25)
|
|
57
|
-
priority_scores = {
|
|
58
|
-
|
|
59
|
-
'high': 75,
|
|
60
|
-
'medium': 50,
|
|
61
|
-
'low': 25
|
|
62
|
-
}
|
|
63
|
-
priority_score = priority_scores.get(d.get('priority', 'medium'), 50)
|
|
60
|
+
priority_scores = {"critical": 100, "high": 75, "medium": 50, "low": 25}
|
|
61
|
+
priority_score = priority_scores.get(d.get("priority", "medium"), 50)
|
|
64
62
|
score += priority_score
|
|
65
|
-
|
|
66
|
-
if d.get(
|
|
67
|
-
reasons.append(
|
|
68
|
-
|
|
63
|
+
|
|
64
|
+
if d.get("priority") == "critical":
|
|
65
|
+
reasons.append("Critical priority")
|
|
66
|
+
|
|
69
67
|
# Phase scoring (earlier phases = higher score)
|
|
70
68
|
phase_scores = {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
"reconnaissance": 90,
|
|
70
|
+
"enumeration": 80,
|
|
71
|
+
"exploitation": 70,
|
|
72
|
+
"post_exploitation": 60,
|
|
73
|
+
"techniques": 50,
|
|
76
74
|
}
|
|
77
|
-
phase_score = phase_scores.get(d[
|
|
75
|
+
phase_score = phase_scores.get(d["category"], 50)
|
|
78
76
|
score += phase_score
|
|
79
|
-
|
|
77
|
+
|
|
80
78
|
# Evidence availability (if we already have related evidence, easier to complete)
|
|
81
|
-
evidence_count = self.em.get_evidence_count(d[
|
|
79
|
+
evidence_count = self.em.get_evidence_count(d["id"])
|
|
82
80
|
if evidence_count > 0:
|
|
83
81
|
score += 20
|
|
84
|
-
reasons.append(f
|
|
85
|
-
|
|
82
|
+
reasons.append(f"{evidence_count} evidence items available")
|
|
83
|
+
|
|
86
84
|
# In-progress items get boost (finish what we started)
|
|
87
|
-
if d[
|
|
85
|
+
if d["status"] == "in_progress":
|
|
88
86
|
score += 30
|
|
89
|
-
reasons.append(
|
|
90
|
-
|
|
87
|
+
reasons.append("Already in progress")
|
|
88
|
+
|
|
91
89
|
# Add time pressure if started long ago
|
|
92
|
-
if d.get(
|
|
90
|
+
if d.get("started_at"):
|
|
93
91
|
try:
|
|
94
|
-
started = datetime.fromisoformat(
|
|
92
|
+
started = datetime.fromisoformat(
|
|
93
|
+
d["started_at"].replace("Z", "+00:00")
|
|
94
|
+
)
|
|
95
95
|
hours_since = (datetime.now() - started).total_seconds() / 3600
|
|
96
96
|
if hours_since > 24:
|
|
97
97
|
score += 15
|
|
98
|
-
reasons.append(f
|
|
98
|
+
reasons.append(f"Started {int(hours_since)}h ago")
|
|
99
99
|
except:
|
|
100
100
|
pass
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
# Blocker penalty
|
|
103
|
-
if d.get(
|
|
103
|
+
if d.get("blocker"):
|
|
104
104
|
score -= 50
|
|
105
|
-
reasons.append(
|
|
106
|
-
|
|
105
|
+
reasons.append("⚠️ BLOCKED")
|
|
106
|
+
|
|
107
107
|
# Auto-validate items (quick wins)
|
|
108
|
-
if d.get(
|
|
108
|
+
if d.get("auto_validate"):
|
|
109
109
|
score += 10
|
|
110
|
-
reasons.append(
|
|
111
|
-
|
|
112
|
-
recommendations.append(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
reasons.append("Auto-validates")
|
|
111
|
+
|
|
112
|
+
recommendations.append(
|
|
113
|
+
{
|
|
114
|
+
"deliverable": d,
|
|
115
|
+
"score": score,
|
|
116
|
+
"reasons": reasons,
|
|
117
|
+
"confidence": min(100, int(score / 3)), # Scale to 0-100
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
119
121
|
# Sort by score descending
|
|
120
|
-
recommendations.sort(key=lambda x: x[
|
|
121
|
-
|
|
122
|
+
recommendations.sort(key=lambda x: x["score"], reverse=True)
|
|
123
|
+
|
|
122
124
|
return recommendations[:10] # Top 10
|
|
123
|
-
|
|
125
|
+
|
|
124
126
|
def _get_blocker_recommendations(self, engagement_id: int) -> List[Dict]:
|
|
125
127
|
"""
|
|
126
128
|
Recommend actions to unblock deliverables.
|
|
127
129
|
"""
|
|
128
130
|
blockers = self.tt.get_blockers(engagement_id)
|
|
129
|
-
|
|
131
|
+
|
|
130
132
|
recommendations = []
|
|
131
|
-
|
|
133
|
+
|
|
132
134
|
for blocker in blockers:
|
|
133
135
|
suggestions = []
|
|
134
|
-
blocker_text = blocker.get(
|
|
135
|
-
|
|
136
|
+
blocker_text = blocker.get("blocker", "").lower()
|
|
137
|
+
|
|
136
138
|
# Pattern matching for common blockers
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
suggestions.append(
|
|
144
|
-
suggestions.append(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
suggestions.append(
|
|
149
|
-
suggestions.append(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
suggestions.append(
|
|
154
|
-
suggestions.append(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
139
|
+
if (
|
|
140
|
+
"credential" in blocker_text
|
|
141
|
+
or "password" in blocker_text
|
|
142
|
+
or "login" in blocker_text
|
|
143
|
+
):
|
|
144
|
+
suggestions.append("Run credential enumeration tools (hydra, medusa)")
|
|
145
|
+
suggestions.append("Check for default credentials")
|
|
146
|
+
suggestions.append("Review already discovered credentials")
|
|
147
|
+
|
|
148
|
+
if "access" in blocker_text or "permission" in blocker_text:
|
|
149
|
+
suggestions.append("Verify network connectivity")
|
|
150
|
+
suggestions.append("Check firewall rules")
|
|
151
|
+
suggestions.append("Request VPN access from client")
|
|
152
|
+
|
|
153
|
+
if "client" in blocker_text or "waiting" in blocker_text:
|
|
154
|
+
suggestions.append("Send follow-up email to client")
|
|
155
|
+
suggestions.append("Schedule status call")
|
|
156
|
+
suggestions.append("Document waiting time for billing")
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
"tool" in blocker_text
|
|
160
|
+
or "error" in blocker_text
|
|
161
|
+
or "fail" in blocker_text
|
|
162
|
+
):
|
|
163
|
+
suggestions.append("Check tool installation")
|
|
164
|
+
suggestions.append("Review error logs")
|
|
165
|
+
suggestions.append("Try alternative tool")
|
|
166
|
+
|
|
167
|
+
if "scope" in blocker_text or "clarification" in blocker_text:
|
|
168
|
+
suggestions.append("Review engagement SOW")
|
|
169
|
+
suggestions.append("Email client for clarification")
|
|
170
|
+
suggestions.append("Document scope questions")
|
|
171
|
+
|
|
162
172
|
# Generic suggestions if no patterns matched
|
|
163
173
|
if not suggestions:
|
|
164
|
-
suggestions.append(
|
|
165
|
-
suggestions.append(
|
|
166
|
-
suggestions.append(
|
|
167
|
-
|
|
168
|
-
recommendations.append(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
suggestions.append("Review blocker details")
|
|
175
|
+
suggestions.append("Consult with team lead")
|
|
176
|
+
suggestions.append("Document resolution steps")
|
|
177
|
+
|
|
178
|
+
recommendations.append(
|
|
179
|
+
{
|
|
180
|
+
"deliverable": blocker,
|
|
181
|
+
"blocker": blocker.get("blocker"),
|
|
182
|
+
"suggestions": suggestions,
|
|
183
|
+
"priority": blocker.get("priority", "medium"),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
175
187
|
return recommendations
|
|
176
|
-
|
|
188
|
+
|
|
177
189
|
def _get_quick_wins(self, engagement_id: int) -> List[Dict]:
|
|
178
190
|
"""
|
|
179
191
|
Identify deliverables that can be completed quickly.
|
|
180
|
-
|
|
192
|
+
|
|
181
193
|
Criteria:
|
|
182
194
|
- Auto-validate enabled
|
|
183
195
|
- Low estimated hours
|
|
@@ -185,108 +197,114 @@ class RecommendationEngine:
|
|
|
185
197
|
- Low priority (save critical for focused time)
|
|
186
198
|
"""
|
|
187
199
|
deliverables = self.dm.list_deliverables(engagement_id)
|
|
188
|
-
|
|
200
|
+
|
|
189
201
|
quick_wins = []
|
|
190
|
-
|
|
202
|
+
|
|
191
203
|
for d in deliverables:
|
|
192
|
-
if d[
|
|
204
|
+
if d["status"] != "pending":
|
|
193
205
|
continue
|
|
194
|
-
|
|
206
|
+
|
|
195
207
|
score = 0
|
|
196
208
|
reasons = []
|
|
197
|
-
|
|
209
|
+
|
|
198
210
|
# Auto-validate = quick
|
|
199
|
-
if d.get(
|
|
211
|
+
if d.get("auto_validate"):
|
|
200
212
|
score += 40
|
|
201
|
-
reasons.append(
|
|
202
|
-
|
|
213
|
+
reasons.append("Auto-validates")
|
|
214
|
+
|
|
203
215
|
# Low estimated hours
|
|
204
|
-
est_hours = d.get(
|
|
216
|
+
est_hours = d.get("estimated_hours", 0)
|
|
205
217
|
if est_hours > 0 and est_hours <= 2:
|
|
206
218
|
score += 30
|
|
207
|
-
reasons.append(f
|
|
219
|
+
reasons.append(f"Quick ({est_hours}h estimated)")
|
|
208
220
|
elif est_hours == 0:
|
|
209
221
|
score += 20 # No estimate, assume might be quick
|
|
210
|
-
|
|
222
|
+
|
|
211
223
|
# Evidence available
|
|
212
|
-
evidence_count = self.em.get_evidence_count(d[
|
|
224
|
+
evidence_count = self.em.get_evidence_count(d["id"])
|
|
213
225
|
if evidence_count > 0:
|
|
214
226
|
score += 20
|
|
215
|
-
reasons.append(f
|
|
216
|
-
|
|
227
|
+
reasons.append(f"{evidence_count} evidence ready")
|
|
228
|
+
|
|
217
229
|
# Boolean target type (simple yes/no)
|
|
218
|
-
if d.get(
|
|
230
|
+
if d.get("target_type") == "boolean":
|
|
219
231
|
score += 15
|
|
220
|
-
reasons.append(
|
|
221
|
-
|
|
232
|
+
reasons.append("Simple yes/no target")
|
|
233
|
+
|
|
222
234
|
# Not critical (save those for focused time)
|
|
223
|
-
if d.get(
|
|
235
|
+
if d.get("priority") in ["low", "medium"]:
|
|
224
236
|
score += 10
|
|
225
|
-
|
|
237
|
+
|
|
226
238
|
if score >= 40: # Threshold for "quick win"
|
|
227
|
-
quick_wins.append(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
quick_wins.append(
|
|
240
|
+
{
|
|
241
|
+
"deliverable": d,
|
|
242
|
+
"score": score,
|
|
243
|
+
"reasons": reasons,
|
|
244
|
+
"estimated_minutes": (
|
|
245
|
+
int(est_hours * 60) if est_hours > 0 else 30
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
234
250
|
# Sort by score
|
|
235
|
-
quick_wins.sort(key=lambda x: x[
|
|
236
|
-
|
|
251
|
+
quick_wins.sort(key=lambda x: x["score"], reverse=True)
|
|
252
|
+
|
|
237
253
|
return quick_wins[:5] # Top 5
|
|
238
|
-
|
|
254
|
+
|
|
239
255
|
def _get_coverage_gaps(self, engagement_id: int) -> List[Dict]:
|
|
240
256
|
"""
|
|
241
257
|
Identify phases or categories with low completion rates.
|
|
242
258
|
"""
|
|
243
259
|
phase_breakdown = self.tt.get_phase_breakdown(engagement_id)
|
|
244
|
-
|
|
260
|
+
|
|
245
261
|
gaps = []
|
|
246
|
-
|
|
262
|
+
|
|
247
263
|
for phase, stats in phase_breakdown.items():
|
|
248
|
-
if stats[
|
|
264
|
+
if stats["total"] == 0:
|
|
249
265
|
continue
|
|
250
|
-
|
|
251
|
-
completion_rate = stats[
|
|
252
|
-
|
|
266
|
+
|
|
267
|
+
completion_rate = stats["completion_rate"]
|
|
268
|
+
|
|
253
269
|
# Flag phases below 50% completion
|
|
254
270
|
if completion_rate < 50:
|
|
255
|
-
severity =
|
|
256
|
-
|
|
257
|
-
gaps.append(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
severity = "critical" if completion_rate < 25 else "high"
|
|
272
|
+
|
|
273
|
+
gaps.append(
|
|
274
|
+
{
|
|
275
|
+
"phase": phase,
|
|
276
|
+
"completion_rate": completion_rate,
|
|
277
|
+
"completed": stats["completed"],
|
|
278
|
+
"total": stats["total"],
|
|
279
|
+
"remaining": stats["total"] - stats["completed"],
|
|
280
|
+
"severity": severity,
|
|
281
|
+
"recommendation": self._get_phase_recommendation(phase, stats),
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
267
285
|
# Sort by completion rate (lowest first)
|
|
268
|
-
gaps.sort(key=lambda x: x[
|
|
269
|
-
|
|
286
|
+
gaps.sort(key=lambda x: x["completion_rate"])
|
|
287
|
+
|
|
270
288
|
return gaps
|
|
271
|
-
|
|
289
|
+
|
|
272
290
|
def _get_phase_recommendation(self, phase: str, stats: Dict) -> str:
|
|
273
291
|
"""Generate phase-specific recommendation."""
|
|
274
|
-
remaining = stats[
|
|
275
|
-
|
|
292
|
+
remaining = stats["total"] - stats["completed"]
|
|
293
|
+
|
|
276
294
|
recommendations = {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
"reconnaissance": f"Run OSINT tools to complete {remaining} recon deliverables",
|
|
296
|
+
"enumeration": f"Enumerate services and users ({remaining} items remaining)",
|
|
297
|
+
"exploitation": f"Test for vulnerabilities - {remaining} exploit deliverables pending",
|
|
298
|
+
"post_exploitation": f"Complete post-exploitation activities ({remaining} remaining)",
|
|
299
|
+
"techniques": f"Document techniques and methodologies ({remaining} items left)",
|
|
282
300
|
}
|
|
283
|
-
|
|
284
|
-
return recommendations.get(phase, f
|
|
285
|
-
|
|
301
|
+
|
|
302
|
+
return recommendations.get(phase, f"Complete {remaining} {phase} deliverables")
|
|
303
|
+
|
|
286
304
|
def _get_at_risk_deliverables(self, engagement_id: int) -> List[Dict]:
|
|
287
305
|
"""
|
|
288
306
|
Identify deliverables at risk of delay.
|
|
289
|
-
|
|
307
|
+
|
|
290
308
|
At risk if:
|
|
291
309
|
- In progress for > 24 hours
|
|
292
310
|
- Critical priority but not started
|
|
@@ -294,126 +312,142 @@ class RecommendationEngine:
|
|
|
294
312
|
- High estimated hours but no progress
|
|
295
313
|
"""
|
|
296
314
|
deliverables = self.dm.list_deliverables(engagement_id)
|
|
297
|
-
|
|
315
|
+
|
|
298
316
|
at_risk = []
|
|
299
|
-
|
|
317
|
+
|
|
300
318
|
for d in deliverables:
|
|
301
|
-
if d[
|
|
319
|
+
if d["status"] == "completed":
|
|
302
320
|
continue
|
|
303
|
-
|
|
321
|
+
|
|
304
322
|
risk_factors = []
|
|
305
323
|
risk_score = 0
|
|
306
|
-
|
|
324
|
+
|
|
307
325
|
# In progress too long
|
|
308
|
-
if d[
|
|
326
|
+
if d["status"] == "in_progress" and d.get("started_at"):
|
|
309
327
|
try:
|
|
310
|
-
started = datetime.fromisoformat(
|
|
328
|
+
started = datetime.fromisoformat(
|
|
329
|
+
d["started_at"].replace("Z", "+00:00")
|
|
330
|
+
)
|
|
311
331
|
hours_since = (datetime.now() - started).total_seconds() / 3600
|
|
312
|
-
|
|
332
|
+
|
|
313
333
|
if hours_since > 48:
|
|
314
334
|
risk_score += 40
|
|
315
|
-
risk_factors.append(f
|
|
335
|
+
risk_factors.append(f"In progress {int(hours_since)}h")
|
|
316
336
|
elif hours_since > 24:
|
|
317
337
|
risk_score += 20
|
|
318
|
-
risk_factors.append(f
|
|
338
|
+
risk_factors.append(f"In progress {int(hours_since)}h")
|
|
319
339
|
except:
|
|
320
340
|
pass
|
|
321
|
-
|
|
341
|
+
|
|
322
342
|
# Critical but not started
|
|
323
|
-
if d.get(
|
|
343
|
+
if d.get("priority") == "critical" and d["status"] == "pending":
|
|
324
344
|
risk_score += 30
|
|
325
|
-
risk_factors.append(
|
|
326
|
-
|
|
345
|
+
risk_factors.append("Critical priority not started")
|
|
346
|
+
|
|
327
347
|
# Has blocker
|
|
328
|
-
if d.get(
|
|
348
|
+
if d.get("blocker"):
|
|
329
349
|
risk_score += 50
|
|
330
|
-
risk_factors.append(
|
|
331
|
-
|
|
350
|
+
risk_factors.append("Blocked")
|
|
351
|
+
|
|
332
352
|
# High estimated hours but pending
|
|
333
|
-
est_hours = d.get(
|
|
334
|
-
if est_hours > 8 and d[
|
|
353
|
+
est_hours = d.get("estimated_hours", 0)
|
|
354
|
+
if est_hours > 8 and d["status"] == "pending":
|
|
335
355
|
risk_score += 15
|
|
336
|
-
risk_factors.append(f
|
|
337
|
-
|
|
356
|
+
risk_factors.append(f"{est_hours}h estimated, not started")
|
|
357
|
+
|
|
338
358
|
if risk_score >= 20: # Risk threshold
|
|
339
|
-
severity =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
359
|
+
severity = (
|
|
360
|
+
"critical"
|
|
361
|
+
if risk_score >= 50
|
|
362
|
+
else ("high" if risk_score >= 35 else "medium")
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
at_risk.append(
|
|
366
|
+
{
|
|
367
|
+
"deliverable": d,
|
|
368
|
+
"risk_score": risk_score,
|
|
369
|
+
"risk_factors": risk_factors,
|
|
370
|
+
"severity": severity,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
348
374
|
# Sort by risk score
|
|
349
|
-
at_risk.sort(key=lambda x: x[
|
|
350
|
-
|
|
375
|
+
at_risk.sort(key=lambda x: x["risk_score"], reverse=True)
|
|
376
|
+
|
|
351
377
|
return at_risk[:5] # Top 5
|
|
352
|
-
|
|
378
|
+
|
|
353
379
|
def _get_priority_boost_suggestions(self, engagement_id: int) -> List[Dict]:
|
|
354
380
|
"""
|
|
355
381
|
Suggest deliverables that should be higher priority.
|
|
356
|
-
|
|
382
|
+
|
|
357
383
|
Based on:
|
|
358
384
|
- Lots of evidence collected (we found something interesting)
|
|
359
385
|
- Related to critical findings
|
|
360
386
|
- Dependency for other deliverables
|
|
361
387
|
"""
|
|
362
388
|
deliverables = self.dm.list_deliverables(engagement_id)
|
|
363
|
-
|
|
389
|
+
|
|
364
390
|
suggestions = []
|
|
365
|
-
|
|
391
|
+
|
|
366
392
|
for d in deliverables:
|
|
367
|
-
if d[
|
|
393
|
+
if d["status"] == "completed":
|
|
368
394
|
continue
|
|
369
|
-
|
|
370
|
-
current_priority = d.get(
|
|
371
|
-
|
|
395
|
+
|
|
396
|
+
current_priority = d.get("priority", "medium")
|
|
397
|
+
|
|
372
398
|
# Skip if already critical
|
|
373
|
-
if current_priority ==
|
|
399
|
+
if current_priority == "critical":
|
|
374
400
|
continue
|
|
375
|
-
|
|
401
|
+
|
|
376
402
|
boost_score = 0
|
|
377
403
|
reasons = []
|
|
378
|
-
|
|
404
|
+
|
|
379
405
|
# High evidence count (we found something interesting)
|
|
380
|
-
evidence = self.em.get_evidence(d[
|
|
381
|
-
|
|
382
|
-
critical_findings = len(
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
evidence = self.em.get_evidence(d["id"])
|
|
407
|
+
|
|
408
|
+
critical_findings = len(
|
|
409
|
+
[f for f in evidence["findings"] if f.get("severity") == "critical"]
|
|
410
|
+
)
|
|
411
|
+
high_findings = len(
|
|
412
|
+
[f for f in evidence["findings"] if f.get("severity") == "high"]
|
|
413
|
+
)
|
|
414
|
+
|
|
385
415
|
if critical_findings > 0:
|
|
386
416
|
boost_score += 50
|
|
387
|
-
reasons.append(f
|
|
388
|
-
|
|
417
|
+
reasons.append(f"{critical_findings} critical findings linked")
|
|
418
|
+
|
|
389
419
|
if high_findings > 0:
|
|
390
420
|
boost_score += 25
|
|
391
|
-
reasons.append(f
|
|
392
|
-
|
|
421
|
+
reasons.append(f"{high_findings} high findings linked")
|
|
422
|
+
|
|
393
423
|
# Lots of evidence in general
|
|
394
|
-
total_evidence = sum(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
424
|
+
total_evidence = sum(
|
|
425
|
+
[
|
|
426
|
+
len(evidence["findings"]),
|
|
427
|
+
len(evidence["credentials"]),
|
|
428
|
+
len(evidence["screenshots"]),
|
|
429
|
+
len(evidence["jobs"]),
|
|
430
|
+
]
|
|
431
|
+
)
|
|
432
|
+
|
|
401
433
|
if total_evidence > 10:
|
|
402
434
|
boost_score += 20
|
|
403
|
-
reasons.append(f
|
|
404
|
-
|
|
435
|
+
reasons.append(f"{total_evidence} evidence items")
|
|
436
|
+
|
|
405
437
|
if boost_score >= 25: # Threshold
|
|
406
|
-
suggested_priority =
|
|
407
|
-
|
|
408
|
-
suggestions.append(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
438
|
+
suggested_priority = "critical" if boost_score >= 50 else "high"
|
|
439
|
+
|
|
440
|
+
suggestions.append(
|
|
441
|
+
{
|
|
442
|
+
"deliverable": d,
|
|
443
|
+
"current_priority": current_priority,
|
|
444
|
+
"suggested_priority": suggested_priority,
|
|
445
|
+
"boost_score": boost_score,
|
|
446
|
+
"reasons": reasons,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
416
450
|
# Sort by boost score
|
|
417
|
-
suggestions.sort(key=lambda x: x[
|
|
418
|
-
|
|
451
|
+
suggestions.sort(key=lambda x: x["boost_score"], reverse=True)
|
|
452
|
+
|
|
419
453
|
return suggestions[:5] # Top 5
|