souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9564 -2881
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +564 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +409 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +417 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +913 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +219 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +237 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23034 -10679
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ Permission levels:
|
|
|
6
6
|
- editor: Can run scans, add findings, but can't delete engagement
|
|
7
7
|
- viewer: Read-only access to engagement data
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import sqlite3
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from typing import Optional, List, Dict, Any
|
|
@@ -17,6 +18,7 @@ from souleyez.auth import get_current_user, Role
|
|
|
17
18
|
|
|
18
19
|
class EngagementPermission(Enum):
|
|
19
20
|
"""Permission levels for engagement access."""
|
|
21
|
+
|
|
20
22
|
OWNER = "owner"
|
|
21
23
|
EDITOR = "editor"
|
|
22
24
|
VIEWER = "viewer"
|
|
@@ -25,6 +27,7 @@ class EngagementPermission(Enum):
|
|
|
25
27
|
@dataclass
|
|
26
28
|
class EngagementAccess:
|
|
27
29
|
"""Represents a user's access to an engagement."""
|
|
30
|
+
|
|
28
31
|
engagement_id: int
|
|
29
32
|
user_id: str
|
|
30
33
|
permission_level: EngagementPermission
|
|
@@ -48,9 +51,7 @@ class EngagementAccessManager:
|
|
|
48
51
|
# =========================================================================
|
|
49
52
|
|
|
50
53
|
def get_user_permission(
|
|
51
|
-
self,
|
|
52
|
-
engagement_id: int,
|
|
53
|
-
user_id: str
|
|
54
|
+
self, engagement_id: int, user_id: str
|
|
54
55
|
) -> Optional[EngagementPermission]:
|
|
55
56
|
"""
|
|
56
57
|
Get user's permission level for an engagement.
|
|
@@ -61,23 +62,22 @@ class EngagementAccessManager:
|
|
|
61
62
|
|
|
62
63
|
# First check if user is the owner
|
|
63
64
|
row = conn.execute(
|
|
64
|
-
"SELECT owner_id FROM engagements WHERE id = ?",
|
|
65
|
-
(engagement_id,)
|
|
65
|
+
"SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
|
|
66
66
|
).fetchone()
|
|
67
67
|
|
|
68
|
-
if row and row[
|
|
68
|
+
if row and row["owner_id"] == user_id:
|
|
69
69
|
conn.close()
|
|
70
70
|
return EngagementPermission.OWNER
|
|
71
71
|
|
|
72
72
|
# Check engagement_permissions table
|
|
73
73
|
row = conn.execute(
|
|
74
74
|
"SELECT permission_level FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
|
|
75
|
-
(engagement_id, user_id)
|
|
75
|
+
(engagement_id, user_id),
|
|
76
76
|
).fetchone()
|
|
77
77
|
conn.close()
|
|
78
78
|
|
|
79
79
|
if row:
|
|
80
|
-
return EngagementPermission(row[
|
|
80
|
+
return EngagementPermission(row["permission_level"])
|
|
81
81
|
|
|
82
82
|
return None
|
|
83
83
|
|
|
@@ -105,7 +105,9 @@ class EngagementAccessManager:
|
|
|
105
105
|
perm = self.get_user_permission(engagement_id, user_id)
|
|
106
106
|
return perm == EngagementPermission.OWNER
|
|
107
107
|
|
|
108
|
-
def can_manage_team(
|
|
108
|
+
def can_manage_team(
|
|
109
|
+
self, engagement_id: int, user_id: str, user_role: Role
|
|
110
|
+
) -> bool:
|
|
109
111
|
"""Check if user can add/remove team members (owner only, or admin)."""
|
|
110
112
|
if user_role == Role.ADMIN:
|
|
111
113
|
return True
|
|
@@ -117,7 +119,9 @@ class EngagementAccessManager:
|
|
|
117
119
|
# Engagement Queries (Filtered by Access)
|
|
118
120
|
# =========================================================================
|
|
119
121
|
|
|
120
|
-
def get_accessible_engagements(
|
|
122
|
+
def get_accessible_engagements(
|
|
123
|
+
self, user_id: str, user_role: Role
|
|
124
|
+
) -> List[Dict[str, Any]]:
|
|
121
125
|
"""
|
|
122
126
|
Get all engagements the user can access.
|
|
123
127
|
|
|
@@ -127,14 +131,17 @@ class EngagementAccessManager:
|
|
|
127
131
|
|
|
128
132
|
if user_role == Role.ADMIN:
|
|
129
133
|
# Admins see everything
|
|
130
|
-
rows = conn.execute(
|
|
134
|
+
rows = conn.execute(
|
|
135
|
+
"""
|
|
131
136
|
SELECT e.*, 'admin' as permission_level
|
|
132
137
|
FROM engagements e
|
|
133
138
|
ORDER BY e.created_at DESC
|
|
134
|
-
"""
|
|
139
|
+
"""
|
|
140
|
+
).fetchall()
|
|
135
141
|
else:
|
|
136
142
|
# Non-admins see owned + shared engagements
|
|
137
|
-
rows = conn.execute(
|
|
143
|
+
rows = conn.execute(
|
|
144
|
+
"""
|
|
138
145
|
SELECT e.*,
|
|
139
146
|
CASE
|
|
140
147
|
WHEN e.owner_id = ? THEN 'owner'
|
|
@@ -147,7 +154,9 @@ class EngagementAccessManager:
|
|
|
147
154
|
OR ep.user_id = ?
|
|
148
155
|
OR e.owner_id IS NULL
|
|
149
156
|
ORDER BY e.created_at DESC
|
|
150
|
-
""",
|
|
157
|
+
""",
|
|
158
|
+
(user_id, user_id, user_id, user_id),
|
|
159
|
+
).fetchall()
|
|
151
160
|
|
|
152
161
|
conn.close()
|
|
153
162
|
return [dict(row) for row in rows]
|
|
@@ -161,7 +170,7 @@ class EngagementAccessManager:
|
|
|
161
170
|
engagement_id: int,
|
|
162
171
|
user_id: str,
|
|
163
172
|
permission_level: EngagementPermission,
|
|
164
|
-
granted_by: str
|
|
173
|
+
granted_by: str,
|
|
165
174
|
) -> tuple[bool, str]:
|
|
166
175
|
"""
|
|
167
176
|
Add a user to an engagement's team.
|
|
@@ -174,45 +183,43 @@ class EngagementAccessManager:
|
|
|
174
183
|
|
|
175
184
|
try:
|
|
176
185
|
conn = self._get_conn()
|
|
177
|
-
conn.execute(
|
|
186
|
+
conn.execute(
|
|
187
|
+
"""
|
|
178
188
|
INSERT OR REPLACE INTO engagement_permissions
|
|
179
189
|
(engagement_id, user_id, permission_level, granted_by, granted_at)
|
|
180
190
|
VALUES (?, ?, ?, ?, ?)
|
|
181
|
-
""",
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
""",
|
|
192
|
+
(
|
|
193
|
+
engagement_id,
|
|
194
|
+
user_id,
|
|
195
|
+
permission_level.value,
|
|
196
|
+
granted_by,
|
|
197
|
+
datetime.now().isoformat(),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
188
200
|
conn.commit()
|
|
189
201
|
conn.close()
|
|
190
202
|
return True, ""
|
|
191
203
|
except Exception as e:
|
|
192
204
|
return False, str(e)
|
|
193
205
|
|
|
194
|
-
def remove_team_member(
|
|
195
|
-
self,
|
|
196
|
-
engagement_id: int,
|
|
197
|
-
user_id: str
|
|
198
|
-
) -> tuple[bool, str]:
|
|
206
|
+
def remove_team_member(self, engagement_id: int, user_id: str) -> tuple[bool, str]:
|
|
199
207
|
"""Remove a user from an engagement's team."""
|
|
200
208
|
try:
|
|
201
209
|
conn = self._get_conn()
|
|
202
210
|
|
|
203
211
|
# Can't remove the owner via this method
|
|
204
212
|
row = conn.execute(
|
|
205
|
-
"SELECT owner_id FROM engagements WHERE id = ?",
|
|
206
|
-
(engagement_id,)
|
|
213
|
+
"SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
|
|
207
214
|
).fetchone()
|
|
208
215
|
|
|
209
|
-
if row and row[
|
|
216
|
+
if row and row["owner_id"] == user_id:
|
|
210
217
|
conn.close()
|
|
211
218
|
return False, "Cannot remove the owner. Transfer ownership first."
|
|
212
219
|
|
|
213
220
|
result = conn.execute(
|
|
214
221
|
"DELETE FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
|
|
215
|
-
(engagement_id, user_id)
|
|
222
|
+
(engagement_id, user_id),
|
|
216
223
|
)
|
|
217
224
|
conn.commit()
|
|
218
225
|
conn.close()
|
|
@@ -229,23 +236,29 @@ class EngagementAccessManager:
|
|
|
229
236
|
conn = self._get_conn()
|
|
230
237
|
|
|
231
238
|
# Get owner
|
|
232
|
-
owner_row = conn.execute(
|
|
239
|
+
owner_row = conn.execute(
|
|
240
|
+
"""
|
|
233
241
|
SELECT u.id, u.username, u.email, 'owner' as permission_level,
|
|
234
242
|
e.created_at as granted_at, NULL as granted_by
|
|
235
243
|
FROM engagements e
|
|
236
244
|
JOIN users u ON e.owner_id = u.id
|
|
237
245
|
WHERE e.id = ?
|
|
238
|
-
""",
|
|
246
|
+
""",
|
|
247
|
+
(engagement_id,),
|
|
248
|
+
).fetchone()
|
|
239
249
|
|
|
240
250
|
# Get other team members
|
|
241
|
-
member_rows = conn.execute(
|
|
251
|
+
member_rows = conn.execute(
|
|
252
|
+
"""
|
|
242
253
|
SELECT u.id, u.username, u.email, ep.permission_level,
|
|
243
254
|
ep.granted_at, ep.granted_by
|
|
244
255
|
FROM engagement_permissions ep
|
|
245
256
|
JOIN users u ON ep.user_id = u.id
|
|
246
257
|
WHERE ep.engagement_id = ?
|
|
247
258
|
ORDER BY ep.granted_at
|
|
248
|
-
""",
|
|
259
|
+
""",
|
|
260
|
+
(engagement_id,),
|
|
261
|
+
).fetchall()
|
|
249
262
|
|
|
250
263
|
conn.close()
|
|
251
264
|
|
|
@@ -257,10 +270,7 @@ class EngagementAccessManager:
|
|
|
257
270
|
return members
|
|
258
271
|
|
|
259
272
|
def transfer_ownership(
|
|
260
|
-
self,
|
|
261
|
-
engagement_id: int,
|
|
262
|
-
new_owner_id: str,
|
|
263
|
-
transferred_by: str
|
|
273
|
+
self, engagement_id: int, new_owner_id: str, transferred_by: str
|
|
264
274
|
) -> tuple[bool, str]:
|
|
265
275
|
"""Transfer engagement ownership to another user."""
|
|
266
276
|
try:
|
|
@@ -268,35 +278,42 @@ class EngagementAccessManager:
|
|
|
268
278
|
|
|
269
279
|
# Get current owner
|
|
270
280
|
row = conn.execute(
|
|
271
|
-
"SELECT owner_id FROM engagements WHERE id = ?",
|
|
272
|
-
(engagement_id,)
|
|
281
|
+
"SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
|
|
273
282
|
).fetchone()
|
|
274
283
|
|
|
275
284
|
if not row:
|
|
276
285
|
conn.close()
|
|
277
286
|
return False, "Engagement not found"
|
|
278
287
|
|
|
279
|
-
old_owner_id = row[
|
|
288
|
+
old_owner_id = row["owner_id"]
|
|
280
289
|
|
|
281
290
|
# Update owner
|
|
282
291
|
conn.execute(
|
|
283
292
|
"UPDATE engagements SET owner_id = ? WHERE id = ?",
|
|
284
|
-
(new_owner_id, engagement_id)
|
|
293
|
+
(new_owner_id, engagement_id),
|
|
285
294
|
)
|
|
286
295
|
|
|
287
296
|
# Remove new owner from permissions table (they're now owner)
|
|
288
297
|
conn.execute(
|
|
289
298
|
"DELETE FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
|
|
290
|
-
(engagement_id, new_owner_id)
|
|
299
|
+
(engagement_id, new_owner_id),
|
|
291
300
|
)
|
|
292
301
|
|
|
293
302
|
# Add old owner as editor (so they don't lose access)
|
|
294
303
|
if old_owner_id:
|
|
295
|
-
conn.execute(
|
|
304
|
+
conn.execute(
|
|
305
|
+
"""
|
|
296
306
|
INSERT OR REPLACE INTO engagement_permissions
|
|
297
307
|
(engagement_id, user_id, permission_level, granted_by, granted_at)
|
|
298
308
|
VALUES (?, ?, 'editor', ?, ?)
|
|
299
|
-
""",
|
|
309
|
+
""",
|
|
310
|
+
(
|
|
311
|
+
engagement_id,
|
|
312
|
+
old_owner_id,
|
|
313
|
+
transferred_by,
|
|
314
|
+
datetime.now().isoformat(),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
300
317
|
|
|
301
318
|
conn.commit()
|
|
302
319
|
conn.close()
|
|
@@ -318,7 +335,7 @@ class EngagementAccessManager:
|
|
|
318
335
|
conn = self._get_conn()
|
|
319
336
|
result = conn.execute(
|
|
320
337
|
"UPDATE engagements SET owner_id = ? WHERE owner_id IS NULL",
|
|
321
|
-
(admin_user_id,)
|
|
338
|
+
(admin_user_id,),
|
|
322
339
|
)
|
|
323
340
|
conn.commit()
|
|
324
341
|
count = result.rowcount
|
souleyez/auth/permissions.py
CHANGED
|
@@ -4,6 +4,7 @@ souleyez.auth.permissions - Role-based access control definitions
|
|
|
4
4
|
Roles: Admin, Lead, Analyst, Viewer
|
|
5
5
|
Tiers: FREE, PRO (for licensing)
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from enum import Enum, auto
|
|
8
9
|
from typing import Set, Optional
|
|
9
10
|
from functools import wraps
|
|
@@ -11,6 +12,7 @@ from functools import wraps
|
|
|
11
12
|
|
|
12
13
|
class Role(Enum):
|
|
13
14
|
"""User roles with hierarchical permissions."""
|
|
15
|
+
|
|
14
16
|
ADMIN = "admin"
|
|
15
17
|
LEAD = "lead"
|
|
16
18
|
ANALYST = "analyst"
|
|
@@ -19,12 +21,14 @@ class Role(Enum):
|
|
|
19
21
|
|
|
20
22
|
class Tier(Enum):
|
|
21
23
|
"""Licensing tiers."""
|
|
24
|
+
|
|
22
25
|
FREE = "FREE"
|
|
23
26
|
PRO = "PRO"
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
class Permission(Enum):
|
|
27
30
|
"""Individual permissions that can be checked."""
|
|
31
|
+
|
|
28
32
|
# User management
|
|
29
33
|
USER_CREATE = auto()
|
|
30
34
|
USER_UPDATE = auto()
|
|
@@ -86,7 +90,6 @@ class Permission(Enum):
|
|
|
86
90
|
# Role -> Permissions mapping
|
|
87
91
|
ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
|
|
88
92
|
Role.ADMIN: set(Permission), # All permissions
|
|
89
|
-
|
|
90
93
|
Role.LEAD: {
|
|
91
94
|
Permission.ENGAGEMENT_CREATE,
|
|
92
95
|
Permission.ENGAGEMENT_UPDATE,
|
|
@@ -116,7 +119,6 @@ ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
|
|
|
116
119
|
Permission.AUDIT_VIEW,
|
|
117
120
|
Permission.USER_LIST,
|
|
118
121
|
},
|
|
119
|
-
|
|
120
122
|
Role.ANALYST: {
|
|
121
123
|
Permission.ENGAGEMENT_VIEW,
|
|
122
124
|
Permission.SCAN_RUN,
|
|
@@ -136,7 +138,6 @@ ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
|
|
|
136
138
|
Permission.AUTOMATION_MANAGE,
|
|
137
139
|
Permission.MSF_INTEGRATION,
|
|
138
140
|
},
|
|
139
|
-
|
|
140
141
|
Role.VIEWER: {
|
|
141
142
|
Permission.ENGAGEMENT_VIEW,
|
|
142
143
|
Permission.FINDING_VIEW,
|
|
@@ -195,11 +196,13 @@ class PermissionChecker:
|
|
|
195
196
|
|
|
196
197
|
def requires_permission(permission: Permission):
|
|
197
198
|
"""Decorator to require a permission for a function."""
|
|
199
|
+
|
|
198
200
|
def decorator(func):
|
|
199
201
|
@wraps(func)
|
|
200
202
|
def wrapper(*args, **kwargs):
|
|
201
203
|
# Get current user from context (implementation depends on how session is managed)
|
|
202
204
|
from souleyez.auth import get_current_user
|
|
205
|
+
|
|
203
206
|
user = get_current_user()
|
|
204
207
|
|
|
205
208
|
if user is None:
|
|
@@ -212,15 +215,19 @@ def requires_permission(permission: Permission):
|
|
|
212
215
|
raise PermissionError(f"Permission denied: {permission.name}")
|
|
213
216
|
|
|
214
217
|
return func(*args, **kwargs)
|
|
218
|
+
|
|
215
219
|
return wrapper
|
|
220
|
+
|
|
216
221
|
return decorator
|
|
217
222
|
|
|
218
223
|
|
|
219
224
|
def requires_pro(func):
|
|
220
225
|
"""Decorator to require Pro tier for a function."""
|
|
226
|
+
|
|
221
227
|
@wraps(func)
|
|
222
228
|
def wrapper(*args, **kwargs):
|
|
223
229
|
from souleyez.auth import get_current_user
|
|
230
|
+
|
|
224
231
|
user = get_current_user()
|
|
225
232
|
|
|
226
233
|
if user is None:
|
|
@@ -230,6 +237,7 @@ def requires_pro(func):
|
|
|
230
237
|
raise PermissionError("Pro license required")
|
|
231
238
|
|
|
232
239
|
return func(*args, **kwargs)
|
|
240
|
+
|
|
233
241
|
return wrapper
|
|
234
242
|
|
|
235
243
|
|
|
@@ -241,6 +249,7 @@ def requires_role(min_role: Role):
|
|
|
241
249
|
@wraps(func)
|
|
242
250
|
def wrapper(*args, **kwargs):
|
|
243
251
|
from souleyez.auth import get_current_user
|
|
252
|
+
|
|
244
253
|
user = get_current_user()
|
|
245
254
|
|
|
246
255
|
if user is None:
|
|
@@ -253,5 +262,7 @@ def requires_role(min_role: Role):
|
|
|
253
262
|
raise PermissionError(f"Requires {min_role.value} role or higher")
|
|
254
263
|
|
|
255
264
|
return func(*args, **kwargs)
|
|
265
|
+
|
|
256
266
|
return wrapper
|
|
267
|
+
|
|
257
268
|
return decorator
|
souleyez/auth/session_manager.py
CHANGED
|
@@ -6,6 +6,7 @@ Handles:
|
|
|
6
6
|
- Session storage and cleanup
|
|
7
7
|
- Current user context
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import sqlite3
|
|
10
11
|
import secrets
|
|
11
12
|
import hashlib
|
|
@@ -28,6 +29,7 @@ SESSION_FILE_NAME = "session.json"
|
|
|
28
29
|
@dataclass
|
|
29
30
|
class Session:
|
|
30
31
|
"""Active user session."""
|
|
32
|
+
|
|
31
33
|
id: str
|
|
32
34
|
user_id: str
|
|
33
35
|
token: str # Only available at creation time
|
|
@@ -78,7 +80,7 @@ class SessionManager:
|
|
|
78
80
|
ip_address: Optional[str] = None,
|
|
79
81
|
user_agent: Optional[str] = None,
|
|
80
82
|
duration_hours: int = DEFAULT_SESSION_HOURS,
|
|
81
|
-
vault_unlocked_at: Optional[datetime] = None
|
|
83
|
+
vault_unlocked_at: Optional[datetime] = None,
|
|
82
84
|
) -> Session:
|
|
83
85
|
"""
|
|
84
86
|
Create a new session for a user.
|
|
@@ -101,18 +103,21 @@ class SessionManager:
|
|
|
101
103
|
expires_at = now + timedelta(hours=duration_hours)
|
|
102
104
|
|
|
103
105
|
conn = self._get_conn()
|
|
104
|
-
conn.execute(
|
|
106
|
+
conn.execute(
|
|
107
|
+
"""
|
|
105
108
|
INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at, ip_address, user_agent)
|
|
106
109
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
107
|
-
""",
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
""",
|
|
111
|
+
(
|
|
112
|
+
session_id,
|
|
113
|
+
user.id,
|
|
114
|
+
token_hash,
|
|
115
|
+
expires_at.isoformat(),
|
|
116
|
+
now.isoformat(),
|
|
117
|
+
ip_address,
|
|
118
|
+
user_agent,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
116
121
|
conn.commit()
|
|
117
122
|
conn.close()
|
|
118
123
|
|
|
@@ -124,7 +129,7 @@ class SessionManager:
|
|
|
124
129
|
created_at=now,
|
|
125
130
|
ip_address=ip_address,
|
|
126
131
|
user_agent=user_agent,
|
|
127
|
-
vault_unlocked_at=vault_unlocked_at or now
|
|
132
|
+
vault_unlocked_at=vault_unlocked_at or now,
|
|
128
133
|
)
|
|
129
134
|
|
|
130
135
|
# Save session to local file
|
|
@@ -142,12 +147,15 @@ class SessionManager:
|
|
|
142
147
|
token_hash = self._hash_token(token)
|
|
143
148
|
|
|
144
149
|
conn = self._get_conn()
|
|
145
|
-
row = conn.execute(
|
|
150
|
+
row = conn.execute(
|
|
151
|
+
"""
|
|
146
152
|
SELECT s.*, u.*
|
|
147
153
|
FROM sessions s
|
|
148
154
|
JOIN users u ON s.user_id = u.id
|
|
149
155
|
WHERE s.token_hash = ? AND s.expires_at > ?
|
|
150
|
-
""",
|
|
156
|
+
""",
|
|
157
|
+
(token_hash, datetime.now().isoformat()),
|
|
158
|
+
).fetchone()
|
|
151
159
|
conn.close()
|
|
152
160
|
|
|
153
161
|
if row is None:
|
|
@@ -155,7 +163,7 @@ class SessionManager:
|
|
|
155
163
|
|
|
156
164
|
# Build user from row
|
|
157
165
|
user_manager = UserManager(self.db_path)
|
|
158
|
-
return user_manager.get_user_by_id(row[
|
|
166
|
+
return user_manager.get_user_by_id(row["user_id"])
|
|
159
167
|
|
|
160
168
|
def invalidate_session(self, session_id: str) -> bool:
|
|
161
169
|
"""Invalidate (logout) a specific session."""
|
|
@@ -187,8 +195,7 @@ class SessionManager:
|
|
|
187
195
|
"""Remove all expired sessions from the database."""
|
|
188
196
|
conn = self._get_conn()
|
|
189
197
|
result = conn.execute(
|
|
190
|
-
"DELETE FROM sessions WHERE expires_at < ?",
|
|
191
|
-
(datetime.now().isoformat(),)
|
|
198
|
+
"DELETE FROM sessions WHERE expires_at < ?", (datetime.now().isoformat(),)
|
|
192
199
|
)
|
|
193
200
|
conn.commit()
|
|
194
201
|
count = result.rowcount
|
|
@@ -202,63 +209,70 @@ class SessionManager:
|
|
|
202
209
|
def is_vault_session_valid(self) -> bool:
|
|
203
210
|
"""
|
|
204
211
|
Check if vault unlock is still valid (30 min inactivity).
|
|
205
|
-
|
|
212
|
+
|
|
206
213
|
Returns:
|
|
207
214
|
True if vault session is still valid
|
|
208
215
|
"""
|
|
209
|
-
if
|
|
216
|
+
if (
|
|
217
|
+
self._current_session is None
|
|
218
|
+
or self._current_session.vault_unlocked_at is None
|
|
219
|
+
):
|
|
210
220
|
return False
|
|
211
|
-
|
|
221
|
+
|
|
212
222
|
elapsed = datetime.now() - self._current_session.vault_unlocked_at
|
|
213
223
|
return elapsed < timedelta(minutes=self._vault_timeout_minutes)
|
|
214
224
|
|
|
215
225
|
def require_full_reauth(self) -> bool:
|
|
216
226
|
"""
|
|
217
227
|
Check if both layers need re-authentication.
|
|
218
|
-
|
|
228
|
+
|
|
219
229
|
Returns:
|
|
220
230
|
True if full re-auth (vault + user) is required
|
|
221
231
|
"""
|
|
222
232
|
if self._current_session is None:
|
|
223
233
|
return True
|
|
224
|
-
|
|
234
|
+
|
|
225
235
|
# Check if vault session has timed out
|
|
226
236
|
if not self.is_vault_session_valid():
|
|
227
237
|
return True
|
|
228
|
-
|
|
238
|
+
|
|
229
239
|
# Check if user session has expired
|
|
230
240
|
if datetime.now() >= self._current_session.expires_at:
|
|
231
241
|
return True
|
|
232
|
-
|
|
242
|
+
|
|
233
243
|
return False
|
|
234
244
|
|
|
235
245
|
def apply_cross_layer_delay(self, vault_failures: int):
|
|
236
246
|
"""
|
|
237
247
|
Apply delay based on vault failures before allowing user login.
|
|
238
|
-
|
|
248
|
+
|
|
239
249
|
Args:
|
|
240
250
|
vault_failures: Number of recent vault unlock failures
|
|
241
251
|
"""
|
|
242
252
|
import time
|
|
243
253
|
import click
|
|
244
|
-
|
|
254
|
+
|
|
245
255
|
if vault_failures >= 2:
|
|
246
256
|
delay = 30 * (vault_failures - 1) # 30s for 2, 60s for 3+
|
|
247
|
-
|
|
257
|
+
|
|
248
258
|
# Avoid re-applying same delay multiple times
|
|
249
259
|
if self._cross_layer_delay_applied_at:
|
|
250
|
-
since_last = (
|
|
260
|
+
since_last = (
|
|
261
|
+
datetime.now() - self._cross_layer_delay_applied_at
|
|
262
|
+
).total_seconds()
|
|
251
263
|
if since_last < delay:
|
|
252
264
|
return
|
|
253
|
-
|
|
254
|
-
click.echo(
|
|
255
|
-
|
|
265
|
+
|
|
266
|
+
click.echo(
|
|
267
|
+
f"\n⚠️ Security delay: {delay} seconds (vault failures detected)"
|
|
268
|
+
)
|
|
269
|
+
|
|
256
270
|
# Show countdown
|
|
257
271
|
for remaining in range(delay, 0, -1):
|
|
258
272
|
click.echo(f"\r Continuing in {remaining}s... ", nl=False)
|
|
259
273
|
time.sleep(1)
|
|
260
274
|
click.echo("\r Continuing... ")
|
|
261
|
-
|
|
275
|
+
|
|
262
276
|
self._cross_layer_delay_applied_at = datetime.now()
|
|
263
277
|
|
|
264
278
|
def _save_session_to_file(self, session: Session):
|
|
@@ -270,7 +284,11 @@ class SessionManager:
|
|
|
270
284
|
"token": session.token,
|
|
271
285
|
"expires_at": session.expires_at.isoformat(),
|
|
272
286
|
"user_id": session.user_id,
|
|
273
|
-
"vault_unlocked_at":
|
|
287
|
+
"vault_unlocked_at": (
|
|
288
|
+
session.vault_unlocked_at.isoformat()
|
|
289
|
+
if session.vault_unlocked_at
|
|
290
|
+
else None
|
|
291
|
+
),
|
|
274
292
|
}
|
|
275
293
|
|
|
276
294
|
self.session_file.write_text(json.dumps(data, indent=2))
|
|
@@ -285,7 +303,7 @@ class SessionManager:
|
|
|
285
303
|
try:
|
|
286
304
|
data = json.loads(self.session_file.read_text())
|
|
287
305
|
# Check expiration
|
|
288
|
-
expires_at = datetime.fromisoformat(data[
|
|
306
|
+
expires_at = datetime.fromisoformat(data["expires_at"])
|
|
289
307
|
if expires_at < datetime.now():
|
|
290
308
|
self._clear_session_file()
|
|
291
309
|
return None
|
|
@@ -317,7 +335,7 @@ class SessionManager:
|
|
|
317
335
|
if session_data is None:
|
|
318
336
|
return None
|
|
319
337
|
|
|
320
|
-
user = self.validate_token(session_data[
|
|
338
|
+
user = self.validate_token(session_data["token"])
|
|
321
339
|
if user is None:
|
|
322
340
|
self._clear_session_file()
|
|
323
341
|
return None
|
|
@@ -337,9 +355,8 @@ class SessionManager:
|
|
|
337
355
|
"""Log out the current user."""
|
|
338
356
|
session_data = self._load_session_from_file()
|
|
339
357
|
if session_data:
|
|
340
|
-
self.invalidate_session(session_data[
|
|
358
|
+
self.invalidate_session(session_data["session_id"])
|
|
341
359
|
|
|
342
360
|
self._clear_session_file()
|
|
343
361
|
self._current_user = None
|
|
344
362
|
return True
|
|
345
|
-
|