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
souleyez/commands/screenshots.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CLI commands for screenshot management.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import click
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from rich.console import Console
|
|
@@ -21,12 +22,12 @@ def screenshots():
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@screenshots.command()
|
|
24
|
-
@click.argument(
|
|
25
|
-
@click.option(
|
|
26
|
-
@click.option(
|
|
27
|
-
@click.option(
|
|
28
|
-
@click.option(
|
|
29
|
-
@click.option(
|
|
25
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
26
|
+
@click.option("--title", "-t", help="Screenshot title")
|
|
27
|
+
@click.option("--description", "-d", help="Screenshot description")
|
|
28
|
+
@click.option("--host", "-h", type=int, help="Link to host ID")
|
|
29
|
+
@click.option("--finding", "-f", type=int, help="Link to finding ID")
|
|
30
|
+
@click.option("--job", "-j", type=int, help="Link to job ID")
|
|
30
31
|
def add(source, title, description, host, finding, job):
|
|
31
32
|
"""Add a screenshot to current engagement."""
|
|
32
33
|
em = EngagementManager()
|
|
@@ -39,13 +40,13 @@ def add(source, title, description, host, finding, job):
|
|
|
39
40
|
|
|
40
41
|
try:
|
|
41
42
|
screenshot_id = sm.add_screenshot(
|
|
42
|
-
engagement_id=current[
|
|
43
|
+
engagement_id=current["id"],
|
|
43
44
|
source_path=source,
|
|
44
45
|
title=title,
|
|
45
46
|
description=description,
|
|
46
47
|
host_id=host,
|
|
47
48
|
finding_id=finding,
|
|
48
|
-
job_id=job
|
|
49
|
+
job_id=job,
|
|
49
50
|
)
|
|
50
51
|
|
|
51
52
|
console.print(f"[green]✓ Screenshot added: ID {screenshot_id}[/green]")
|
|
@@ -64,9 +65,9 @@ def add(source, title, description, host, finding, job):
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
@screenshots.command()
|
|
67
|
-
@click.option(
|
|
68
|
-
@click.option(
|
|
69
|
-
@click.option(
|
|
68
|
+
@click.option("--host", "-h", type=int, help="Filter by host ID")
|
|
69
|
+
@click.option("--finding", "-f", type=int, help="Filter by finding ID")
|
|
70
|
+
@click.option("--job", "-j", type=int, help="Filter by job ID")
|
|
70
71
|
def list(host, finding, job):
|
|
71
72
|
"""List screenshots for current engagement."""
|
|
72
73
|
em = EngagementManager()
|
|
@@ -78,10 +79,7 @@ def list(host, finding, job):
|
|
|
78
79
|
return
|
|
79
80
|
|
|
80
81
|
screenshots = sm.list_screenshots(
|
|
81
|
-
engagement_id=current[
|
|
82
|
-
host_id=host,
|
|
83
|
-
finding_id=finding,
|
|
84
|
-
job_id=job
|
|
82
|
+
engagement_id=current["id"], host_id=host, finding_id=finding, job_id=job
|
|
85
83
|
)
|
|
86
84
|
|
|
87
85
|
if not screenshots:
|
|
@@ -97,7 +95,7 @@ def list(host, finding, job):
|
|
|
97
95
|
table.add_column("Created", style="blue")
|
|
98
96
|
|
|
99
97
|
for s in screenshots:
|
|
100
|
-
size = s[
|
|
98
|
+
size = s["file_size"]
|
|
101
99
|
if size < 1024:
|
|
102
100
|
size_str = f"{size} B"
|
|
103
101
|
elif size < 1024 * 1024:
|
|
@@ -106,21 +104,21 @@ def list(host, finding, job):
|
|
|
106
104
|
size_str = f"{size / (1024 * 1024):.1f} MB"
|
|
107
105
|
|
|
108
106
|
links = []
|
|
109
|
-
if s[
|
|
107
|
+
if s["host_id"]:
|
|
110
108
|
links.append(f"Host:{s['host_id']}")
|
|
111
|
-
if s[
|
|
109
|
+
if s["finding_id"]:
|
|
112
110
|
links.append(f"Finding:{s['finding_id']}")
|
|
113
|
-
if s[
|
|
111
|
+
if s["job_id"]:
|
|
114
112
|
links.append(f"Job:{s['job_id']}")
|
|
115
113
|
links_str = ", ".join(links) if links else "None"
|
|
116
114
|
|
|
117
115
|
table.add_row(
|
|
118
|
-
str(s[
|
|
119
|
-
s[
|
|
120
|
-
s[
|
|
116
|
+
str(s["id"]),
|
|
117
|
+
s["title"] or s["filename"],
|
|
118
|
+
s["filename"][:30] + "..." if len(s["filename"]) > 30 else s["filename"],
|
|
121
119
|
size_str,
|
|
122
120
|
links_str,
|
|
123
|
-
s[
|
|
121
|
+
s["created_at"][:10],
|
|
124
122
|
)
|
|
125
123
|
|
|
126
124
|
console.print(table)
|
|
@@ -128,30 +126,36 @@ def list(host, finding, job):
|
|
|
128
126
|
|
|
129
127
|
|
|
130
128
|
@screenshots.command()
|
|
131
|
-
@click.argument(
|
|
132
|
-
@click.option(
|
|
133
|
-
@click.option(
|
|
134
|
-
@click.option(
|
|
129
|
+
@click.argument("screenshot_id", type=int)
|
|
130
|
+
@click.option("--host", "-h", type=int, help="Link to host ID")
|
|
131
|
+
@click.option("--finding", "-f", type=int, help="Link to finding ID")
|
|
132
|
+
@click.option("--job", "-j", type=int, help="Link to job ID")
|
|
135
133
|
def link(screenshot_id, host, finding, job):
|
|
136
134
|
"""Link screenshot to host/finding/job."""
|
|
137
135
|
sm = ScreenshotManager()
|
|
138
136
|
|
|
139
137
|
if host:
|
|
140
138
|
sm.link_to_host(screenshot_id, host)
|
|
141
|
-
console.print(
|
|
139
|
+
console.print(
|
|
140
|
+
f"[green]✓ Linked screenshot {screenshot_id} to host {host}[/green]"
|
|
141
|
+
)
|
|
142
142
|
|
|
143
143
|
if finding:
|
|
144
144
|
sm.link_to_finding(screenshot_id, finding)
|
|
145
|
-
console.print(
|
|
145
|
+
console.print(
|
|
146
|
+
f"[green]✓ Linked screenshot {screenshot_id} to finding {finding}[/green]"
|
|
147
|
+
)
|
|
146
148
|
|
|
147
149
|
if job:
|
|
148
150
|
sm.link_to_job(screenshot_id, job)
|
|
149
|
-
console.print(
|
|
151
|
+
console.print(
|
|
152
|
+
f"[green]✓ Linked screenshot {screenshot_id} to job {job}[/green]"
|
|
153
|
+
)
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
@screenshots.command()
|
|
153
|
-
@click.argument(
|
|
154
|
-
@click.confirmation_option(prompt=
|
|
157
|
+
@click.argument("screenshot_id", type=int)
|
|
158
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this screenshot?")
|
|
155
159
|
def delete(screenshot_id):
|
|
156
160
|
"""Delete a screenshot."""
|
|
157
161
|
sm = ScreenshotManager()
|
souleyez/commands/user.py
CHANGED
|
@@ -8,6 +8,7 @@ Commands (admin only):
|
|
|
8
8
|
- souleyez user delete <username> - Delete user
|
|
9
9
|
- souleyez user passwd [username] - Change password
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
import click
|
|
12
13
|
import getpass
|
|
13
14
|
from rich.console import Console
|
|
@@ -45,14 +46,20 @@ def user():
|
|
|
45
46
|
@require_admin
|
|
46
47
|
@click.argument("username")
|
|
47
48
|
@click.option("--email", "-e", help="User email address")
|
|
48
|
-
@click.option(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
@click.option(
|
|
50
|
+
"--role",
|
|
51
|
+
"-r",
|
|
52
|
+
type=click.Choice(["admin", "lead", "analyst", "viewer"]),
|
|
53
|
+
default="analyst",
|
|
54
|
+
help="User role (default: analyst)",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--tier",
|
|
58
|
+
"-t",
|
|
59
|
+
type=click.Choice(["FREE", "PRO"]),
|
|
60
|
+
default=None,
|
|
61
|
+
help="License tier (auto-detects from active license)",
|
|
62
|
+
)
|
|
56
63
|
def user_create(username, email, role, tier):
|
|
57
64
|
"""Create a new user account."""
|
|
58
65
|
import secrets
|
|
@@ -77,7 +84,7 @@ def user_create(username, email, role, tier):
|
|
|
77
84
|
email=email,
|
|
78
85
|
role=Role(role),
|
|
79
86
|
tier=Tier(tier),
|
|
80
|
-
skip_password_validation=True # Generated password is secure
|
|
87
|
+
skip_password_validation=True, # Generated password is secure
|
|
81
88
|
)
|
|
82
89
|
|
|
83
90
|
if new_user is None:
|
|
@@ -86,8 +93,12 @@ def user_create(username, email, role, tier):
|
|
|
86
93
|
|
|
87
94
|
# Log the action
|
|
88
95
|
current = get_current_user()
|
|
89
|
-
_log_audit(
|
|
90
|
-
|
|
96
|
+
_log_audit(
|
|
97
|
+
"user.created",
|
|
98
|
+
current.id,
|
|
99
|
+
current.username,
|
|
100
|
+
f"Created user: {username} (role={role}, tier={tier})",
|
|
101
|
+
)
|
|
91
102
|
|
|
92
103
|
# Display credentials in a nice panel
|
|
93
104
|
tier_display = "💎 PRO" if tier == "PRO" else "FREE"
|
|
@@ -101,7 +112,9 @@ def user_create(username, email, role, tier):
|
|
|
101
112
|
f"[dim]To change password after login:[/dim]\n"
|
|
102
113
|
f"[dim] • souleyez user passwd {username}[/dim]"
|
|
103
114
|
)
|
|
104
|
-
console.print(
|
|
115
|
+
console.print(
|
|
116
|
+
Panel(panel_content, title="🔐 New User Created", border_style="green")
|
|
117
|
+
)
|
|
105
118
|
|
|
106
119
|
|
|
107
120
|
@user.command("list")
|
|
@@ -130,7 +143,11 @@ def user_list(show_all):
|
|
|
130
143
|
status = "[green]Active[/green]" if u.is_active else "[red]Disabled[/red]"
|
|
131
144
|
if u.is_locked:
|
|
132
145
|
status = "[yellow]Locked[/yellow]"
|
|
133
|
-
last_login =
|
|
146
|
+
last_login = (
|
|
147
|
+
u.last_login.strftime("%Y-%m-%d %H:%M")
|
|
148
|
+
if u.last_login
|
|
149
|
+
else "[dim]Never[/dim]"
|
|
150
|
+
)
|
|
134
151
|
|
|
135
152
|
table.add_row(
|
|
136
153
|
u.username,
|
|
@@ -138,7 +155,7 @@ def user_list(show_all):
|
|
|
138
155
|
tier_badge,
|
|
139
156
|
u.email or "[dim]-[/dim]",
|
|
140
157
|
status,
|
|
141
|
-
last_login
|
|
158
|
+
last_login,
|
|
142
159
|
)
|
|
143
160
|
|
|
144
161
|
console.print(table)
|
|
@@ -149,14 +166,17 @@ def user_list(show_all):
|
|
|
149
166
|
@require_login
|
|
150
167
|
@require_admin
|
|
151
168
|
@click.argument("username")
|
|
152
|
-
@click.option(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
@click.option(
|
|
170
|
+
"--role",
|
|
171
|
+
"-r",
|
|
172
|
+
type=click.Choice(["admin", "lead", "analyst", "viewer"]),
|
|
173
|
+
help="New role",
|
|
174
|
+
)
|
|
175
|
+
@click.option("--tier", "-t", type=click.Choice(["FREE", "PRO"]), help="New tier")
|
|
158
176
|
@click.option("--email", "-e", help="New email")
|
|
159
|
-
@click.option(
|
|
177
|
+
@click.option(
|
|
178
|
+
"--activate/--deactivate", default=None, help="Activate or deactivate account"
|
|
179
|
+
)
|
|
160
180
|
def user_update(username, role, tier, email, activate):
|
|
161
181
|
"""Update a user's role, tier, or status."""
|
|
162
182
|
user_mgr = _get_user_manager()
|
|
@@ -178,7 +198,9 @@ def user_update(username, role, tier, email, activate):
|
|
|
178
198
|
updates["is_active"] = activate
|
|
179
199
|
|
|
180
200
|
if not updates:
|
|
181
|
-
console.print(
|
|
201
|
+
console.print(
|
|
202
|
+
"[yellow]No changes specified. Use --role, --tier, --email, or --activate/--deactivate[/yellow]"
|
|
203
|
+
)
|
|
182
204
|
return
|
|
183
205
|
|
|
184
206
|
success, error = user_mgr.update_user(target.id, **updates)
|
|
@@ -189,8 +211,12 @@ def user_update(username, role, tier, email, activate):
|
|
|
189
211
|
|
|
190
212
|
# Log the action
|
|
191
213
|
current = get_current_user()
|
|
192
|
-
_log_audit(
|
|
193
|
-
|
|
214
|
+
_log_audit(
|
|
215
|
+
"user.updated",
|
|
216
|
+
current.id,
|
|
217
|
+
current.username,
|
|
218
|
+
f"Updated user: {username} ({updates})",
|
|
219
|
+
)
|
|
194
220
|
|
|
195
221
|
console.print(f"[green]✅ User '{username}' updated successfully![/green]")
|
|
196
222
|
|
|
@@ -227,8 +253,9 @@ def user_delete(username, force):
|
|
|
227
253
|
console.print(f"[red]❌ {error}[/red]")
|
|
228
254
|
return
|
|
229
255
|
|
|
230
|
-
_log_audit(
|
|
231
|
-
|
|
256
|
+
_log_audit(
|
|
257
|
+
"user.deleted", current.id, current.username, f"Deleted user: {username}"
|
|
258
|
+
)
|
|
232
259
|
|
|
233
260
|
console.print(f"[green]✅ User '{username}' deleted[/green]")
|
|
234
261
|
|
|
@@ -245,7 +272,9 @@ def user_passwd(username):
|
|
|
245
272
|
if username:
|
|
246
273
|
# Changing another user's password - requires admin
|
|
247
274
|
if current.role != Role.ADMIN:
|
|
248
|
-
console.print(
|
|
275
|
+
console.print(
|
|
276
|
+
"[red]❌ Admin privileges required to change another user's password[/red]"
|
|
277
|
+
)
|
|
249
278
|
return
|
|
250
279
|
target = user_mgr.get_user_by_username(username)
|
|
251
280
|
if target is None:
|
|
@@ -283,8 +312,12 @@ def user_passwd(username):
|
|
|
283
312
|
console.print(f"[red]❌ {error}[/red]")
|
|
284
313
|
return
|
|
285
314
|
|
|
286
|
-
_log_audit(
|
|
287
|
-
|
|
315
|
+
_log_audit(
|
|
316
|
+
"user.password_changed",
|
|
317
|
+
current.id,
|
|
318
|
+
current.username,
|
|
319
|
+
f"Password changed for: {username}",
|
|
320
|
+
)
|
|
288
321
|
|
|
289
322
|
console.print(f"[green]✅ Password changed successfully![/green]")
|
|
290
323
|
|
|
@@ -315,8 +348,12 @@ def user_upgrade(username, reason):
|
|
|
315
348
|
console.print(f"[red]❌ {error}[/red]")
|
|
316
349
|
return
|
|
317
350
|
|
|
318
|
-
_log_audit(
|
|
319
|
-
|
|
351
|
+
_log_audit(
|
|
352
|
+
"user.tier_upgraded",
|
|
353
|
+
current.id,
|
|
354
|
+
current.username,
|
|
355
|
+
f"Upgraded '{username}' to PRO. Reason: {reason}",
|
|
356
|
+
)
|
|
320
357
|
|
|
321
358
|
console.print(f"[green]✅ User '{username}' upgraded to 💎 PRO[/green]")
|
|
322
359
|
console.print(f" Reason: {reason}")
|
|
@@ -344,7 +381,9 @@ def user_downgrade(username, reason, force):
|
|
|
344
381
|
|
|
345
382
|
# Confirm downgrade
|
|
346
383
|
if not force:
|
|
347
|
-
console.print(
|
|
384
|
+
console.print(
|
|
385
|
+
f"\n[yellow]⚠️ This will remove Pro features for '{username}'[/yellow]"
|
|
386
|
+
)
|
|
348
387
|
if not click.confirm("Continue?"):
|
|
349
388
|
console.print("[dim]Cancelled[/dim]")
|
|
350
389
|
return
|
|
@@ -356,8 +395,12 @@ def user_downgrade(username, reason, force):
|
|
|
356
395
|
console.print(f"[red]❌ {error}[/red]")
|
|
357
396
|
return
|
|
358
397
|
|
|
359
|
-
_log_audit(
|
|
360
|
-
|
|
398
|
+
_log_audit(
|
|
399
|
+
"user.tier_downgraded",
|
|
400
|
+
current.id,
|
|
401
|
+
current.username,
|
|
402
|
+
f"Downgraded '{username}' to FREE. Reason: {reason}",
|
|
403
|
+
)
|
|
361
404
|
|
|
362
405
|
console.print(f"[green]✅ User '{username}' downgraded to FREE[/green]")
|
|
363
406
|
console.print(f" Reason: {reason}")
|
|
@@ -371,10 +414,13 @@ def _log_audit(action: str, user_id: str, username: str, details: str = None):
|
|
|
371
414
|
|
|
372
415
|
try:
|
|
373
416
|
conn = sqlite3.connect(get_db().db_path)
|
|
374
|
-
conn.execute(
|
|
417
|
+
conn.execute(
|
|
418
|
+
"""
|
|
375
419
|
INSERT INTO audit_log (user_id, username, action, details, timestamp)
|
|
376
420
|
VALUES (?, ?, ?, ?, ?)
|
|
377
|
-
""",
|
|
421
|
+
""",
|
|
422
|
+
(user_id, username, action, details, datetime.now().isoformat()),
|
|
423
|
+
)
|
|
378
424
|
conn.commit()
|
|
379
425
|
conn.close()
|
|
380
426
|
except Exception:
|
souleyez/config.py
CHANGED
|
@@ -28,29 +28,34 @@ CONFIG_PATH = Path.home() / ".souleyez" / "config.json"
|
|
|
28
28
|
|
|
29
29
|
DEFAULT_CONFIG = {
|
|
30
30
|
"plugins": {"enabled": [], "disabled": []},
|
|
31
|
-
"settings": {
|
|
31
|
+
"settings": {
|
|
32
|
+
"wordlists": None,
|
|
33
|
+
"proxy": None,
|
|
34
|
+
"threads": 10,
|
|
35
|
+
"ollama_model": "llama3.1:8b",
|
|
36
|
+
},
|
|
32
37
|
"database": {
|
|
33
38
|
"path": "~/.souleyez/souleyez.db",
|
|
34
39
|
"backup_enabled": True,
|
|
35
|
-
"backup_interval_hours": 24
|
|
40
|
+
"backup_interval_hours": 24,
|
|
36
41
|
},
|
|
37
42
|
"crypto": {
|
|
38
43
|
"algorithm": "AES-256-GCM",
|
|
39
44
|
"iterations": 600000,
|
|
40
|
-
"key_derivation": "PBKDF2"
|
|
45
|
+
"key_derivation": "PBKDF2",
|
|
41
46
|
},
|
|
42
47
|
"logging": {
|
|
43
48
|
"level": "INFO",
|
|
44
49
|
"format": "json",
|
|
45
50
|
"file": "~/.souleyez/souleyez.log",
|
|
46
51
|
"max_bytes": 10485760,
|
|
47
|
-
"backup_count": 5
|
|
52
|
+
"backup_count": 5,
|
|
48
53
|
},
|
|
49
54
|
"security": {
|
|
50
55
|
"session_timeout_minutes": 30,
|
|
51
56
|
"max_login_attempts": 5,
|
|
52
57
|
"lockout_duration_minutes": 15,
|
|
53
|
-
"min_password_length": 12
|
|
58
|
+
"min_password_length": 12,
|
|
54
59
|
},
|
|
55
60
|
"ai": {
|
|
56
61
|
"provider": "ollama", # "ollama" or "claude"
|
|
@@ -65,7 +70,7 @@ DEFAULT_CONFIG = {
|
|
|
65
70
|
# AI Chain Advisor settings
|
|
66
71
|
"chain_mode": "suggest", # "off", "suggest", or "auto"
|
|
67
72
|
"chain_min_confidence": 0.6, # Minimum confidence for AI recommendations
|
|
68
|
-
"chain_max_recommendations": 5 # Max AI suggestions per analysis
|
|
73
|
+
"chain_max_recommendations": 5, # Max AI suggestions per analysis
|
|
69
74
|
},
|
|
70
75
|
# MSF RPC Configuration (Pro feature)
|
|
71
76
|
"msfrpc": {
|
|
@@ -78,8 +83,8 @@ DEFAULT_CONFIG = {
|
|
|
78
83
|
"timeout": 30, # Connection timeout in seconds
|
|
79
84
|
"poll_interval": 2, # Seconds between session polls
|
|
80
85
|
"max_poll_time": 300, # Max time to wait for session (5 min)
|
|
81
|
-
"fallback_to_console": True # Use msfconsole if RPC unavailable
|
|
82
|
-
}
|
|
86
|
+
"fallback_to_console": True, # Use msfconsole if RPC unavailable
|
|
87
|
+
},
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
CONFIG_SCHEMA = {
|
|
@@ -87,87 +92,86 @@ CONFIG_SCHEMA = {
|
|
|
87
92
|
"type": int,
|
|
88
93
|
"min": 100000,
|
|
89
94
|
"max": 10000000,
|
|
90
|
-
"error": "Iterations must be between 100k and 10M for security"
|
|
95
|
+
"error": "Iterations must be between 100k and 10M for security",
|
|
91
96
|
},
|
|
92
97
|
"database.path": {
|
|
93
98
|
"type": str,
|
|
94
|
-
"validator": lambda p: not p.startswith(
|
|
95
|
-
|
|
96
|
-
),
|
|
97
|
-
"error": "Database path must be local filesystem"
|
|
99
|
+
"validator": lambda p: not p.startswith(("http://", "https://", "ftp://")),
|
|
100
|
+
"error": "Database path must be local filesystem",
|
|
98
101
|
},
|
|
99
102
|
"logging.level": {
|
|
100
103
|
"type": str,
|
|
101
104
|
"allowed": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
102
|
-
"error": "Invalid log level"
|
|
105
|
+
"error": "Invalid log level",
|
|
103
106
|
},
|
|
104
107
|
"security.max_login_attempts": {
|
|
105
108
|
"type": int,
|
|
106
109
|
"min": 1,
|
|
107
110
|
"max": 10,
|
|
108
|
-
"error": "Max login attempts must be 1-10"
|
|
111
|
+
"error": "Max login attempts must be 1-10",
|
|
109
112
|
},
|
|
110
113
|
"security.session_timeout_minutes": {
|
|
111
114
|
"type": int,
|
|
112
115
|
"min": 5,
|
|
113
116
|
"max": 1440,
|
|
114
|
-
"error": "Session timeout must be 5-1440 minutes"
|
|
117
|
+
"error": "Session timeout must be 5-1440 minutes",
|
|
115
118
|
},
|
|
116
119
|
"settings.threads": {
|
|
117
120
|
"type": int,
|
|
118
121
|
"min": 1,
|
|
119
122
|
"max": 100,
|
|
120
|
-
"error": "Threads must be 1-100"
|
|
123
|
+
"error": "Threads must be 1-100",
|
|
121
124
|
},
|
|
122
125
|
"ai.ollama_mode": {
|
|
123
126
|
"type": str,
|
|
124
127
|
"allowed": ["local", "remote"],
|
|
125
|
-
"error": "Ollama mode must be 'local' or 'remote'"
|
|
128
|
+
"error": "Ollama mode must be 'local' or 'remote'",
|
|
126
129
|
},
|
|
127
130
|
"ai.chain_mode": {
|
|
128
131
|
"type": str,
|
|
129
132
|
"allowed": ["off", "suggest", "auto"],
|
|
130
|
-
"error": "AI chain mode must be 'off', 'suggest', or 'auto'"
|
|
133
|
+
"error": "AI chain mode must be 'off', 'suggest', or 'auto'",
|
|
131
134
|
},
|
|
132
135
|
"ai.chain_min_confidence": {
|
|
133
136
|
"type": float,
|
|
134
137
|
"min": 0.0,
|
|
135
138
|
"max": 1.0,
|
|
136
|
-
"error": "AI chain confidence must be 0.0-1.0"
|
|
139
|
+
"error": "AI chain confidence must be 0.0-1.0",
|
|
137
140
|
},
|
|
138
141
|
"ai.chain_max_recommendations": {
|
|
139
142
|
"type": int,
|
|
140
143
|
"min": 1,
|
|
141
144
|
"max": 20,
|
|
142
|
-
"error": "AI chain max recommendations must be 1-20"
|
|
145
|
+
"error": "AI chain max recommendations must be 1-20",
|
|
143
146
|
},
|
|
144
147
|
# MSF RPC validation
|
|
145
148
|
"msfrpc.port": {
|
|
146
149
|
"type": int,
|
|
147
150
|
"min": 1,
|
|
148
151
|
"max": 65535,
|
|
149
|
-
"error": "MSF RPC port must be 1-65535"
|
|
152
|
+
"error": "MSF RPC port must be 1-65535",
|
|
150
153
|
},
|
|
151
154
|
"msfrpc.timeout": {
|
|
152
155
|
"type": int,
|
|
153
156
|
"min": 5,
|
|
154
157
|
"max": 120,
|
|
155
|
-
"error": "MSF RPC timeout must be 5-120 seconds"
|
|
158
|
+
"error": "MSF RPC timeout must be 5-120 seconds",
|
|
156
159
|
},
|
|
157
160
|
"msfrpc.poll_interval": {
|
|
158
161
|
"type": int,
|
|
159
162
|
"min": 1,
|
|
160
163
|
"max": 30,
|
|
161
|
-
"error": "Poll interval must be 1-30 seconds"
|
|
164
|
+
"error": "Poll interval must be 1-30 seconds",
|
|
162
165
|
},
|
|
163
166
|
"msfrpc.max_poll_time": {
|
|
164
167
|
"type": int,
|
|
165
168
|
"min": 30,
|
|
166
169
|
"max": 600,
|
|
167
|
-
"error": "Max poll time must be 30-600 seconds"
|
|
168
|
-
}
|
|
170
|
+
"error": "Max poll time must be 30-600 seconds",
|
|
171
|
+
},
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
|
|
171
175
|
def _ensure_dir():
|
|
172
176
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
173
177
|
|
|
@@ -177,7 +181,7 @@ def _get_nested(data: dict, key: str, default=None):
|
|
|
177
181
|
Get nested dict value using dotted notation.
|
|
178
182
|
Example: _get_nested(cfg, 'database.path') -> cfg['database']['path']
|
|
179
183
|
"""
|
|
180
|
-
parts = key.split(
|
|
184
|
+
parts = key.split(".")
|
|
181
185
|
current = data
|
|
182
186
|
for part in parts:
|
|
183
187
|
if isinstance(current, dict) and part in current:
|
|
@@ -193,7 +197,7 @@ def _set_nested(data: dict, key: str, value):
|
|
|
193
197
|
Example: _set_nested(cfg, 'database.path', '/foo')
|
|
194
198
|
-> cfg['database']['path'] = '/foo'
|
|
195
199
|
"""
|
|
196
|
-
parts = key.split(
|
|
200
|
+
parts = key.split(".")
|
|
197
201
|
current = data
|
|
198
202
|
for part in parts[:-1]:
|
|
199
203
|
if part not in current:
|
|
@@ -237,12 +241,14 @@ def validate_config(cfg: dict) -> tuple[bool, list[str]]:
|
|
|
237
241
|
def _merge_with_defaults(cfg: dict) -> dict:
|
|
238
242
|
"""Deep merge user config with defaults."""
|
|
239
243
|
import copy
|
|
244
|
+
|
|
240
245
|
merged = copy.deepcopy(DEFAULT_CONFIG)
|
|
241
246
|
|
|
242
247
|
def deep_merge(base, updates):
|
|
243
248
|
for key, value in updates.items():
|
|
244
|
-
is_dict_merge = (
|
|
245
|
-
|
|
249
|
+
is_dict_merge = (
|
|
250
|
+
key in base and isinstance(base[key], dict) and isinstance(value, dict)
|
|
251
|
+
)
|
|
246
252
|
if is_dict_merge:
|
|
247
253
|
deep_merge(base[key], value)
|
|
248
254
|
else:
|
|
@@ -252,7 +258,6 @@ def _merge_with_defaults(cfg: dict) -> dict:
|
|
|
252
258
|
return merged
|
|
253
259
|
|
|
254
260
|
|
|
255
|
-
|
|
256
261
|
def _normalize(data: dict) -> dict:
|
|
257
262
|
# Accept both new {"plugins":{...}}
|
|
258
263
|
# and old flat {"enabled":[], "disabled":[]}
|
|
@@ -264,7 +269,12 @@ def _normalize(data: dict) -> dict:
|
|
|
264
269
|
plugins.setdefault("disabled", [])
|
|
265
270
|
data.setdefault(
|
|
266
271
|
"settings",
|
|
267
|
-
{
|
|
272
|
+
{
|
|
273
|
+
"wordlists": None,
|
|
274
|
+
"proxy": None,
|
|
275
|
+
"threads": 10,
|
|
276
|
+
"ollama_model": "llama3.1:8b",
|
|
277
|
+
},
|
|
268
278
|
)
|
|
269
279
|
return data
|
|
270
280
|
# old flat form
|
|
@@ -272,11 +282,15 @@ def _normalize(data: dict) -> dict:
|
|
|
272
282
|
disabled = data.get("disabled", []) or []
|
|
273
283
|
return {
|
|
274
284
|
"plugins": {"enabled": enabled, "disabled": disabled},
|
|
275
|
-
"settings": {
|
|
285
|
+
"settings": {
|
|
286
|
+
"wordlists": None,
|
|
287
|
+
"proxy": None,
|
|
288
|
+
"threads": 10,
|
|
289
|
+
"ollama_model": "llama3.1:8b",
|
|
290
|
+
},
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
|
|
279
|
-
|
|
280
294
|
def read_config() -> dict:
|
|
281
295
|
"""
|
|
282
296
|
Read and validate config from file.
|
|
@@ -292,8 +306,7 @@ def read_config() -> dict:
|
|
|
292
306
|
return DEFAULT_CONFIG.copy()
|
|
293
307
|
except Exception as e:
|
|
294
308
|
logging.warning(
|
|
295
|
-
f"Cannot create config file: {e}. "
|
|
296
|
-
"Using defaults in memory."
|
|
309
|
+
f"Cannot create config file: {e}. " "Using defaults in memory."
|
|
297
310
|
)
|
|
298
311
|
return DEFAULT_CONFIG.copy()
|
|
299
312
|
|
|
@@ -312,12 +325,8 @@ def read_config() -> dict:
|
|
|
312
325
|
return normalized
|
|
313
326
|
|
|
314
327
|
except json.JSONDecodeError as e:
|
|
315
|
-
logging.error(
|
|
316
|
-
|
|
317
|
-
)
|
|
318
|
-
logging.warning(
|
|
319
|
-
"Using default config. Fix or delete config file to reset."
|
|
320
|
-
)
|
|
328
|
+
logging.error(f"Corrupted config file at {CONFIG_PATH}: {e}")
|
|
329
|
+
logging.warning("Using default config. Fix or delete config file to reset.")
|
|
321
330
|
return DEFAULT_CONFIG.copy()
|
|
322
331
|
except Exception as e:
|
|
323
332
|
logging.error(f"Error reading config: {e}")
|
|
@@ -339,7 +348,7 @@ def get(key: str, default=None):
|
|
|
339
348
|
get('database.path') -> checks SOULEYEZ_DATABASE_PATH env,
|
|
340
349
|
then config file, then default
|
|
341
350
|
"""
|
|
342
|
-
env_key = "SOULEYEZ_" + key.upper().replace(
|
|
351
|
+
env_key = "SOULEYEZ_" + key.upper().replace(".", "_")
|
|
343
352
|
env_val = os.getenv(env_key)
|
|
344
353
|
if env_val is not None:
|
|
345
354
|
return env_val
|
|
@@ -352,7 +361,6 @@ def get(key: str, default=None):
|
|
|
352
361
|
return _get_nested(DEFAULT_CONFIG, key, default)
|
|
353
362
|
|
|
354
363
|
|
|
355
|
-
|
|
356
364
|
def list_plugins_config() -> tuple[list[str], list[str]]:
|
|
357
365
|
cfg = read_config()
|
|
358
366
|
e = [x.lower() for x in cfg["plugins"]["enabled"]]
|