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.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +22827 -10678
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/auth/user_manager.py
CHANGED
|
@@ -7,6 +7,7 @@ Handles:
|
|
|
7
7
|
- Default admin user creation
|
|
8
8
|
- Tier management for licensing
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import sqlite3
|
|
11
12
|
import hashlib
|
|
12
13
|
import secrets
|
|
@@ -27,6 +28,7 @@ MIN_PASSWORD_LENGTH = 8
|
|
|
27
28
|
@dataclass
|
|
28
29
|
class User:
|
|
29
30
|
"""User account model."""
|
|
31
|
+
|
|
30
32
|
id: str
|
|
31
33
|
username: str
|
|
32
34
|
email: Optional[str]
|
|
@@ -95,10 +97,7 @@ class UserManager:
|
|
|
95
97
|
Hex-encoded password hash
|
|
96
98
|
"""
|
|
97
99
|
key = hashlib.pbkdf2_hmac(
|
|
98
|
-
|
|
99
|
-
password.encode('utf-8'),
|
|
100
|
-
bytes.fromhex(salt),
|
|
101
|
-
HASH_ITERATIONS
|
|
100
|
+
"sha256", password.encode("utf-8"), bytes.fromhex(salt), HASH_ITERATIONS
|
|
102
101
|
)
|
|
103
102
|
return key.hex()
|
|
104
103
|
|
|
@@ -125,13 +124,13 @@ class UserManager:
|
|
|
125
124
|
if len(password) < MIN_PASSWORD_LENGTH:
|
|
126
125
|
return False, f"Password must be at least {MIN_PASSWORD_LENGTH} characters"
|
|
127
126
|
|
|
128
|
-
if not re.search(r
|
|
127
|
+
if not re.search(r"[A-Z]", password):
|
|
129
128
|
return False, "Password must contain at least one uppercase letter"
|
|
130
129
|
|
|
131
|
-
if not re.search(r
|
|
130
|
+
if not re.search(r"[a-z]", password):
|
|
132
131
|
return False, "Password must contain at least one lowercase letter"
|
|
133
132
|
|
|
134
|
-
if not re.search(r
|
|
133
|
+
if not re.search(r"\d", password):
|
|
135
134
|
return False, "Password must contain at least one digit"
|
|
136
135
|
|
|
137
136
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
@@ -154,7 +153,7 @@ class UserManager:
|
|
|
154
153
|
email: Optional[str] = None,
|
|
155
154
|
role: Role = Role.ANALYST,
|
|
156
155
|
tier: Tier = Tier.FREE,
|
|
157
|
-
skip_password_validation: bool = False
|
|
156
|
+
skip_password_validation: bool = False,
|
|
158
157
|
) -> tuple[Optional[User], str]:
|
|
159
158
|
"""
|
|
160
159
|
Create a new user account.
|
|
@@ -174,8 +173,11 @@ class UserManager:
|
|
|
174
173
|
if not username or len(username) < 3:
|
|
175
174
|
return None, "Username must be at least 3 characters"
|
|
176
175
|
|
|
177
|
-
if not re.match(r
|
|
178
|
-
return
|
|
176
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", username):
|
|
177
|
+
return (
|
|
178
|
+
None,
|
|
179
|
+
"Username can only contain letters, numbers, underscores, and hyphens",
|
|
180
|
+
)
|
|
179
181
|
|
|
180
182
|
# Validate password
|
|
181
183
|
if not skip_password_validation:
|
|
@@ -192,15 +194,26 @@ class UserManager:
|
|
|
192
194
|
|
|
193
195
|
try:
|
|
194
196
|
conn = self._get_conn()
|
|
195
|
-
conn.execute(
|
|
197
|
+
conn.execute(
|
|
198
|
+
"""
|
|
196
199
|
INSERT INTO users (
|
|
197
200
|
id, username, password_hash, salt, email, role, tier,
|
|
198
201
|
is_active, created_at, updated_at
|
|
199
202
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
200
|
-
""",
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
""",
|
|
204
|
+
(
|
|
205
|
+
user_id,
|
|
206
|
+
username.lower(),
|
|
207
|
+
password_hash,
|
|
208
|
+
salt,
|
|
209
|
+
email,
|
|
210
|
+
role.value,
|
|
211
|
+
tier.value,
|
|
212
|
+
True,
|
|
213
|
+
now,
|
|
214
|
+
now,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
204
217
|
conn.commit()
|
|
205
218
|
conn.close()
|
|
206
219
|
|
|
@@ -214,9 +227,7 @@ class UserManager:
|
|
|
214
227
|
def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
215
228
|
"""Get user by ID."""
|
|
216
229
|
conn = self._get_conn()
|
|
217
|
-
row = conn.execute(
|
|
218
|
-
"SELECT * FROM users WHERE id = ?", (user_id,)
|
|
219
|
-
).fetchone()
|
|
230
|
+
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
220
231
|
conn.close()
|
|
221
232
|
|
|
222
233
|
if row is None:
|
|
@@ -257,7 +268,7 @@ class UserManager:
|
|
|
257
268
|
email: Optional[str] = None,
|
|
258
269
|
role: Optional[Role] = None,
|
|
259
270
|
tier: Optional[Tier] = None,
|
|
260
|
-
is_active: Optional[bool] = None
|
|
271
|
+
is_active: Optional[bool] = None,
|
|
261
272
|
) -> tuple[bool, str]:
|
|
262
273
|
"""
|
|
263
274
|
Update user fields.
|
|
@@ -295,7 +306,7 @@ class UserManager:
|
|
|
295
306
|
conn = self._get_conn()
|
|
296
307
|
result = conn.execute(
|
|
297
308
|
f"UPDATE users SET {', '.join(updates)} WHERE id = ?", # nosec B608 - column names are whitelisted, not user input
|
|
298
|
-
params
|
|
309
|
+
params,
|
|
299
310
|
)
|
|
300
311
|
conn.commit()
|
|
301
312
|
conn.close()
|
|
@@ -309,10 +320,7 @@ class UserManager:
|
|
|
309
320
|
return False, f"Failed to update user: {e}"
|
|
310
321
|
|
|
311
322
|
def change_password(
|
|
312
|
-
self,
|
|
313
|
-
user_id: str,
|
|
314
|
-
new_password: str,
|
|
315
|
-
skip_validation: bool = False
|
|
323
|
+
self, user_id: str, new_password: str, skip_validation: bool = False
|
|
316
324
|
) -> tuple[bool, str]:
|
|
317
325
|
"""Change user password."""
|
|
318
326
|
if not skip_validation:
|
|
@@ -325,11 +333,14 @@ class UserManager:
|
|
|
325
333
|
|
|
326
334
|
try:
|
|
327
335
|
conn = self._get_conn()
|
|
328
|
-
result = conn.execute(
|
|
336
|
+
result = conn.execute(
|
|
337
|
+
"""
|
|
329
338
|
UPDATE users
|
|
330
339
|
SET password_hash = ?, salt = ?, updated_at = ?
|
|
331
340
|
WHERE id = ?
|
|
332
|
-
""",
|
|
341
|
+
""",
|
|
342
|
+
(password_hash, salt, datetime.now().isoformat(), user_id),
|
|
343
|
+
)
|
|
333
344
|
conn.commit()
|
|
334
345
|
conn.close()
|
|
335
346
|
|
|
@@ -397,11 +408,14 @@ class UserManager:
|
|
|
397
408
|
return None, "Invalid username or password"
|
|
398
409
|
|
|
399
410
|
# Successful login - reset failed attempts and update last_login
|
|
400
|
-
conn.execute(
|
|
411
|
+
conn.execute(
|
|
412
|
+
"""
|
|
401
413
|
UPDATE users
|
|
402
414
|
SET failed_login_attempts = 0, locked_until = NULL, last_login = ?
|
|
403
415
|
WHERE id = ?
|
|
404
|
-
""",
|
|
416
|
+
""",
|
|
417
|
+
(datetime.now().isoformat(), user.id),
|
|
418
|
+
)
|
|
405
419
|
conn.commit()
|
|
406
420
|
conn.close()
|
|
407
421
|
|
|
@@ -412,11 +426,14 @@ class UserManager:
|
|
|
412
426
|
MAX_ATTEMPTS = 5
|
|
413
427
|
LOCKOUT_MINUTES = 15
|
|
414
428
|
|
|
415
|
-
conn.execute(
|
|
429
|
+
conn.execute(
|
|
430
|
+
"""
|
|
416
431
|
UPDATE users
|
|
417
432
|
SET failed_login_attempts = failed_login_attempts + 1
|
|
418
433
|
WHERE id = ?
|
|
419
|
-
""",
|
|
434
|
+
""",
|
|
435
|
+
(user_id,),
|
|
436
|
+
)
|
|
420
437
|
|
|
421
438
|
# Check if we need to lock the account
|
|
422
439
|
row = conn.execute(
|
|
@@ -425,10 +442,14 @@ class UserManager:
|
|
|
425
442
|
|
|
426
443
|
if row and row[0] >= MAX_ATTEMPTS:
|
|
427
444
|
from datetime import timedelta
|
|
445
|
+
|
|
428
446
|
locked_until = datetime.now() + timedelta(minutes=LOCKOUT_MINUTES)
|
|
429
|
-
conn.execute(
|
|
447
|
+
conn.execute(
|
|
448
|
+
"""
|
|
430
449
|
UPDATE users SET locked_until = ? WHERE id = ?
|
|
431
|
-
""",
|
|
450
|
+
""",
|
|
451
|
+
(locked_until.isoformat(), user_id),
|
|
452
|
+
)
|
|
432
453
|
|
|
433
454
|
conn.commit()
|
|
434
455
|
|
|
@@ -462,7 +483,7 @@ class UserManager:
|
|
|
462
483
|
password=default_password,
|
|
463
484
|
role=Role.ADMIN,
|
|
464
485
|
tier=Tier.FREE, # Start FREE, upgrade when license is activated
|
|
465
|
-
skip_password_validation=True
|
|
486
|
+
skip_password_validation=True,
|
|
466
487
|
)
|
|
467
488
|
|
|
468
489
|
if user is None:
|
|
@@ -488,7 +509,7 @@ class UserManager:
|
|
|
488
509
|
tier: Tier,
|
|
489
510
|
license_key: Optional[str] = None,
|
|
490
511
|
expires_at: Optional[datetime] = None,
|
|
491
|
-
_bypass_validation: bool = False
|
|
512
|
+
_bypass_validation: bool = False,
|
|
492
513
|
) -> tuple[bool, str]:
|
|
493
514
|
"""
|
|
494
515
|
Set user's license tier.
|
|
@@ -510,10 +531,14 @@ class UserManager:
|
|
|
510
531
|
if tier == Tier.PRO and not _bypass_validation:
|
|
511
532
|
try:
|
|
512
533
|
from souleyez.licensing import get_active_license
|
|
534
|
+
|
|
513
535
|
license_info = get_active_license()
|
|
514
536
|
|
|
515
537
|
if not license_info or not license_info.is_valid:
|
|
516
|
-
return
|
|
538
|
+
return (
|
|
539
|
+
False,
|
|
540
|
+
"Valid license required for Pro tier. Use 'souleyez license activate <key>'",
|
|
541
|
+
)
|
|
517
542
|
|
|
518
543
|
# Use license info for expiration
|
|
519
544
|
expires_at = license_info.expires_at
|
|
@@ -525,17 +550,20 @@ class UserManager:
|
|
|
525
550
|
|
|
526
551
|
try:
|
|
527
552
|
conn = self._get_conn()
|
|
528
|
-
conn.execute(
|
|
553
|
+
conn.execute(
|
|
554
|
+
"""
|
|
529
555
|
UPDATE users
|
|
530
556
|
SET tier = ?, license_key = ?, license_expires_at = ?, updated_at = ?
|
|
531
557
|
WHERE id = ?
|
|
532
|
-
""",
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
558
|
+
""",
|
|
559
|
+
(
|
|
560
|
+
tier.value,
|
|
561
|
+
license_key,
|
|
562
|
+
expires_at.isoformat() if expires_at else None,
|
|
563
|
+
datetime.now().isoformat(),
|
|
564
|
+
user_id,
|
|
565
|
+
),
|
|
566
|
+
)
|
|
539
567
|
conn.commit()
|
|
540
568
|
conn.close()
|
|
541
569
|
return True, ""
|
|
@@ -554,15 +582,14 @@ class UserManager:
|
|
|
554
582
|
"""
|
|
555
583
|
try:
|
|
556
584
|
conn = self._get_conn()
|
|
557
|
-
cursor = conn.execute(
|
|
585
|
+
cursor = conn.execute(
|
|
586
|
+
"""
|
|
558
587
|
UPDATE users
|
|
559
588
|
SET tier = ?, license_key = NULL, license_expires_at = NULL, updated_at = ?
|
|
560
589
|
WHERE tier = ?
|
|
561
|
-
""",
|
|
562
|
-
Tier.FREE.value,
|
|
563
|
-
|
|
564
|
-
Tier.PRO.value
|
|
565
|
-
))
|
|
590
|
+
""",
|
|
591
|
+
(Tier.FREE.value, datetime.now().isoformat(), Tier.PRO.value),
|
|
592
|
+
)
|
|
566
593
|
count = cursor.rowcount
|
|
567
594
|
conn.commit()
|
|
568
595
|
conn.close()
|
|
@@ -577,21 +604,39 @@ class UserManager:
|
|
|
577
604
|
def _row_to_user(self, row: sqlite3.Row) -> User:
|
|
578
605
|
"""Convert database row to User object."""
|
|
579
606
|
user = User(
|
|
580
|
-
id=row[
|
|
581
|
-
username=row[
|
|
582
|
-
email=row[
|
|
583
|
-
role=Role(row[
|
|
584
|
-
tier=Tier(row[
|
|
585
|
-
is_active=bool(row[
|
|
586
|
-
created_at=
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
id=row["id"],
|
|
608
|
+
username=row["username"],
|
|
609
|
+
email=row["email"],
|
|
610
|
+
role=Role(row["role"]),
|
|
611
|
+
tier=Tier(row["tier"]),
|
|
612
|
+
is_active=bool(row["is_active"]),
|
|
613
|
+
created_at=(
|
|
614
|
+
datetime.fromisoformat(row["created_at"])
|
|
615
|
+
if row["created_at"]
|
|
616
|
+
else datetime.now()
|
|
617
|
+
),
|
|
618
|
+
updated_at=(
|
|
619
|
+
datetime.fromisoformat(row["updated_at"])
|
|
620
|
+
if row["updated_at"]
|
|
621
|
+
else datetime.now()
|
|
622
|
+
),
|
|
623
|
+
last_login=(
|
|
624
|
+
datetime.fromisoformat(row["last_login"]) if row["last_login"] else None
|
|
625
|
+
),
|
|
626
|
+
license_key=row["license_key"],
|
|
627
|
+
license_expires_at=(
|
|
628
|
+
datetime.fromisoformat(row["license_expires_at"])
|
|
629
|
+
if row["license_expires_at"]
|
|
630
|
+
else None
|
|
631
|
+
),
|
|
632
|
+
failed_login_attempts=row["failed_login_attempts"] or 0,
|
|
633
|
+
locked_until=(
|
|
634
|
+
datetime.fromisoformat(row["locked_until"])
|
|
635
|
+
if row["locked_until"]
|
|
636
|
+
else None
|
|
637
|
+
),
|
|
593
638
|
)
|
|
594
639
|
# Set password fields (private)
|
|
595
|
-
user._password_hash = row[
|
|
596
|
-
user._salt = row[
|
|
640
|
+
user._password_hash = row["password_hash"]
|
|
641
|
+
user._salt = row["salt"]
|
|
597
642
|
return user
|
souleyez/commands/audit.py
CHANGED
|
@@ -7,6 +7,7 @@ Commands:
|
|
|
7
7
|
- souleyez audit stats - Show audit statistics
|
|
8
8
|
- souleyez audit export - Export audit logs
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import click
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
from rich.console import Console
|
|
@@ -38,11 +39,7 @@ def audit_list(limit, user, action):
|
|
|
38
39
|
"""List recent audit log entries."""
|
|
39
40
|
logger = get_audit_logger()
|
|
40
41
|
|
|
41
|
-
entries = logger.query(
|
|
42
|
-
username=user,
|
|
43
|
-
action=action,
|
|
44
|
-
limit=limit
|
|
45
|
-
)
|
|
42
|
+
entries = logger.query(username=user, action=action, limit=limit)
|
|
46
43
|
|
|
47
44
|
if not entries:
|
|
48
45
|
console.print("[yellow]No audit entries found[/yellow]")
|
|
@@ -58,45 +55,45 @@ def audit_list(limit, user, action):
|
|
|
58
55
|
|
|
59
56
|
for e in entries:
|
|
60
57
|
# Parse timestamp
|
|
61
|
-
ts = e[
|
|
58
|
+
ts = e["timestamp"][:19].replace("T", " ")
|
|
62
59
|
|
|
63
60
|
# Format resource
|
|
64
61
|
resource = ""
|
|
65
|
-
if e[
|
|
62
|
+
if e["resource_type"]:
|
|
66
63
|
resource = f"{e['resource_type']}"
|
|
67
|
-
if e[
|
|
64
|
+
if e["resource_id"]:
|
|
68
65
|
resource += f":{e['resource_id']}"
|
|
69
66
|
|
|
70
67
|
# Format details (truncate if long)
|
|
71
68
|
details = ""
|
|
72
|
-
if e[
|
|
69
|
+
if e["details"]:
|
|
73
70
|
try:
|
|
74
|
-
d = json.loads(e[
|
|
71
|
+
d = json.loads(e["details"])
|
|
75
72
|
details = str(d)[:30]
|
|
76
73
|
except:
|
|
77
|
-
details = e[
|
|
74
|
+
details = e["details"][:30]
|
|
78
75
|
|
|
79
76
|
# Success indicator
|
|
80
|
-
success = "[green]✓[/green]" if e[
|
|
77
|
+
success = "[green]✓[/green]" if e["success"] else "[red]✗[/red]"
|
|
81
78
|
|
|
82
79
|
# Color action by category
|
|
83
|
-
action_str = e[
|
|
84
|
-
if action_str.startswith(
|
|
80
|
+
action_str = e["action"]
|
|
81
|
+
if action_str.startswith("auth.") or action_str.startswith("permission."):
|
|
85
82
|
action_str = f"[red]{action_str}[/red]"
|
|
86
|
-
elif action_str.startswith(
|
|
83
|
+
elif action_str.startswith("user."):
|
|
87
84
|
action_str = f"[cyan]{action_str}[/cyan]"
|
|
88
|
-
elif action_str.startswith(
|
|
85
|
+
elif action_str.startswith("scan."):
|
|
89
86
|
action_str = f"[yellow]{action_str}[/yellow]"
|
|
90
|
-
elif action_str.startswith(
|
|
87
|
+
elif action_str.startswith("engagement."):
|
|
91
88
|
action_str = f"[green]{action_str}[/green]"
|
|
92
89
|
|
|
93
90
|
table.add_row(
|
|
94
91
|
ts,
|
|
95
|
-
e[
|
|
92
|
+
e["username"] or "-",
|
|
96
93
|
action_str,
|
|
97
|
-
resource or
|
|
98
|
-
details or
|
|
99
|
-
success
|
|
94
|
+
resource or "-",
|
|
95
|
+
details or "-",
|
|
96
|
+
success,
|
|
100
97
|
)
|
|
101
98
|
|
|
102
99
|
console.print(table)
|
|
@@ -126,7 +123,7 @@ def audit_search(user, action, resource, start, end, failed, limit):
|
|
|
126
123
|
start_date=start_date,
|
|
127
124
|
end_date=end_date,
|
|
128
125
|
success_only=not failed,
|
|
129
|
-
limit=limit
|
|
126
|
+
limit=limit,
|
|
130
127
|
)
|
|
131
128
|
|
|
132
129
|
if not entries:
|
|
@@ -136,18 +133,18 @@ def audit_search(user, action, resource, start, end, failed, limit):
|
|
|
136
133
|
console.print(f"[green]Found {len(entries)} entries[/green]\n")
|
|
137
134
|
|
|
138
135
|
for e in entries:
|
|
139
|
-
ts = e[
|
|
140
|
-
success = "✓" if e[
|
|
136
|
+
ts = e["timestamp"][:19].replace("T", " ")
|
|
137
|
+
success = "✓" if e["success"] else "✗"
|
|
141
138
|
|
|
142
139
|
console.print(f"[dim]{ts}[/dim] [{success}] [bold]{e['action']}[/bold]")
|
|
143
140
|
console.print(f" User: {e['username'] or 'system'}")
|
|
144
141
|
|
|
145
|
-
if e[
|
|
142
|
+
if e["resource_type"]:
|
|
146
143
|
console.print(f" Resource: {e['resource_type']}:{e['resource_id'] or ''}")
|
|
147
144
|
|
|
148
|
-
if e[
|
|
145
|
+
if e["details"]:
|
|
149
146
|
try:
|
|
150
|
-
details = json.loads(e[
|
|
147
|
+
details = json.loads(e["details"])
|
|
151
148
|
console.print(f" Details: {details}")
|
|
152
149
|
except:
|
|
153
150
|
console.print(f" Details: {e['details']}")
|
|
@@ -164,22 +161,24 @@ def audit_stats(days):
|
|
|
164
161
|
logger = get_audit_logger()
|
|
165
162
|
stats = logger.get_stats(days)
|
|
166
163
|
|
|
167
|
-
console.print(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
console.print(
|
|
165
|
+
Panel(
|
|
166
|
+
f"[bold]Period:[/bold] Last {stats['period_days']} days\n"
|
|
167
|
+
f"[bold]Total Events:[/bold] {stats['total_events']}\n"
|
|
168
|
+
f"[bold]Failed Events:[/bold] [red]{stats['failed_events']}[/red]\n"
|
|
169
|
+
f"[bold]Unique Users:[/bold] {stats['unique_users']}",
|
|
170
|
+
title="📊 Audit Statistics",
|
|
171
|
+
border_style="blue",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
175
174
|
|
|
176
|
-
if stats[
|
|
175
|
+
if stats["by_category"]:
|
|
177
176
|
console.print("\n[bold]Events by Category:[/bold]")
|
|
178
177
|
table = Table(show_header=False, box=None)
|
|
179
178
|
table.add_column("Category", width=20)
|
|
180
179
|
table.add_column("Count", justify="right")
|
|
181
180
|
|
|
182
|
-
for cat, count in sorted(stats[
|
|
181
|
+
for cat, count in sorted(stats["by_category"].items(), key=lambda x: -x[1]):
|
|
183
182
|
bar = "█" * min(count // 10, 30)
|
|
184
183
|
table.add_row(cat, f"{count} {bar}")
|
|
185
184
|
|
|
@@ -203,9 +202,7 @@ def audit_export(start, end, format, output):
|
|
|
203
202
|
end_date = datetime.fromisoformat(end) if end else datetime.now()
|
|
204
203
|
|
|
205
204
|
entries = logger.query(
|
|
206
|
-
start_date=start_date,
|
|
207
|
-
end_date=end_date,
|
|
208
|
-
limit=10000 # Large limit for export
|
|
205
|
+
start_date=start_date, end_date=end_date, limit=10000 # Large limit for export
|
|
209
206
|
)
|
|
210
207
|
|
|
211
208
|
if not entries:
|
|
@@ -214,14 +211,14 @@ def audit_export(start, end, format, output):
|
|
|
214
211
|
|
|
215
212
|
# Generate filename if not provided
|
|
216
213
|
if not output:
|
|
217
|
-
date_str = start_date.strftime(
|
|
214
|
+
date_str = start_date.strftime("%Y%m%d")
|
|
218
215
|
output = f"audit_log_{date_str}.{format}"
|
|
219
216
|
|
|
220
217
|
if format == "json":
|
|
221
|
-
with open(output,
|
|
218
|
+
with open(output, "w") as f:
|
|
222
219
|
json.dump(entries, f, indent=2, default=str)
|
|
223
220
|
else:
|
|
224
|
-
with open(output,
|
|
221
|
+
with open(output, "w", newline="") as f:
|
|
225
222
|
writer = csv.DictWriter(f, fieldnames=entries[0].keys())
|
|
226
223
|
writer.writeheader()
|
|
227
224
|
writer.writerows(entries)
|
souleyez/commands/auth.py
CHANGED
|
@@ -6,6 +6,7 @@ Commands:
|
|
|
6
6
|
- souleyez logout - Log out of current session
|
|
7
7
|
- souleyez whoami - Show current user info
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import click
|
|
10
11
|
import getpass
|
|
11
12
|
from rich.console import Console
|
|
@@ -51,17 +52,21 @@ def login():
|
|
|
51
52
|
# Check for first-run (no users exist)
|
|
52
53
|
user_mgr = UserManager(get_db().db_path)
|
|
53
54
|
if user_mgr.get_user_count() == 0:
|
|
54
|
-
console.print(
|
|
55
|
+
console.print(
|
|
56
|
+
"[yellow]No users found. Creating default admin account...[/yellow]"
|
|
57
|
+
)
|
|
55
58
|
created, password = user_mgr.ensure_default_admin()
|
|
56
59
|
if created:
|
|
57
|
-
console.print(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
console.print(
|
|
61
|
+
Panel(
|
|
62
|
+
f"[green]Default admin account created![/green]\n\n"
|
|
63
|
+
f"Username: [bold]admin[/bold]\n"
|
|
64
|
+
f"Password: [bold]{password}[/bold]\n\n"
|
|
65
|
+
f"[red]⚠️ Save this password! It will not be shown again.[/red]",
|
|
66
|
+
title="🔐 First Run Setup",
|
|
67
|
+
border_style="green",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
65
70
|
|
|
66
71
|
# Prompt for credentials
|
|
67
72
|
console.print("\n[bold]🔐 SoulEyez Login[/bold]\n")
|
|
@@ -88,7 +93,9 @@ def login():
|
|
|
88
93
|
console.print(f"\n[green]✅ Welcome, {user.username}![/green]")
|
|
89
94
|
console.print(f" Role: [cyan]{user.role.value.upper()}[/cyan]")
|
|
90
95
|
console.print(f" Tier: [magenta]{tier_badge}[/magenta]")
|
|
91
|
-
console.print(
|
|
96
|
+
console.print(
|
|
97
|
+
f" Session expires: {session.expires_at.strftime('%Y-%m-%d %H:%M')}\n"
|
|
98
|
+
)
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
@click.command()
|
|
@@ -134,11 +141,21 @@ def whoami():
|
|
|
134
141
|
table.add_row("Role", f"[cyan]{user.role.value.upper()}[/cyan]")
|
|
135
142
|
table.add_row("Tier", f"[magenta]{tier_badge}[/magenta]")
|
|
136
143
|
table.add_row("Email", user.email or "[dim]Not set[/dim]")
|
|
137
|
-
table.add_row(
|
|
138
|
-
|
|
144
|
+
table.add_row(
|
|
145
|
+
"Last Login",
|
|
146
|
+
(
|
|
147
|
+
user.last_login.strftime("%Y-%m-%d %H:%M")
|
|
148
|
+
if user.last_login
|
|
149
|
+
else "[dim]Never[/dim]"
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
table.add_row(
|
|
153
|
+
"Account Status",
|
|
154
|
+
"[green]Active[/green]" if user.is_active else "[red]Disabled[/red]",
|
|
155
|
+
)
|
|
139
156
|
|
|
140
157
|
if user.license_expires_at:
|
|
141
|
-
table.add_row("License Expires", user.license_expires_at.strftime(
|
|
158
|
+
table.add_row("License Expires", user.license_expires_at.strftime("%Y-%m-%d"))
|
|
142
159
|
|
|
143
160
|
console.print(Panel(table, title="👤 Current User", border_style="blue"))
|
|
144
161
|
|
|
@@ -150,10 +167,13 @@ def _log_audit(action: str, user_id: str, username: str, details: str = None):
|
|
|
150
167
|
|
|
151
168
|
try:
|
|
152
169
|
conn = sqlite3.connect(get_db().db_path)
|
|
153
|
-
conn.execute(
|
|
170
|
+
conn.execute(
|
|
171
|
+
"""
|
|
154
172
|
INSERT INTO audit_log (user_id, username, action, details, timestamp)
|
|
155
173
|
VALUES (?, ?, ?, ?, ?)
|
|
156
|
-
""",
|
|
174
|
+
""",
|
|
175
|
+
(user_id, username, action, details, datetime.now().isoformat()),
|
|
176
|
+
)
|
|
157
177
|
conn.commit()
|
|
158
178
|
conn.close()
|
|
159
179
|
except Exception:
|