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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CLI commands for deliverable tracking.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import click
|
|
5
6
|
from rich.console import Console
|
|
6
7
|
from rich.table import Table
|
|
7
8
|
from rich.progress import Progress, BarColumn, TextColumn
|
|
9
|
+
|
|
8
10
|
try:
|
|
9
11
|
from rich.progress import TaskProgressColumn
|
|
10
12
|
except ImportError:
|
|
@@ -25,7 +27,7 @@ def deliverables():
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
@deliverables.command()
|
|
28
|
-
@click.option(
|
|
30
|
+
@click.option("--defaults", is_flag=True, help="Create default deliverables")
|
|
29
31
|
def init(defaults):
|
|
30
32
|
"""Initialize deliverables for current engagement."""
|
|
31
33
|
em = EngagementManager()
|
|
@@ -37,15 +39,15 @@ def init(defaults):
|
|
|
37
39
|
return
|
|
38
40
|
|
|
39
41
|
if defaults:
|
|
40
|
-
count = dm.create_default_deliverables(current[
|
|
42
|
+
count = dm.create_default_deliverables(current["id"])
|
|
41
43
|
console.print(f"[green]✓ Created {count} default deliverables[/green]")
|
|
42
44
|
else:
|
|
43
45
|
console.print("[yellow]Use --defaults to create default deliverables[/yellow]")
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
@deliverables.command()
|
|
47
|
-
@click.option(
|
|
48
|
-
@click.option(
|
|
49
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
50
|
+
@click.option("--status", "-s", help="Filter by status")
|
|
49
51
|
def list(category, status):
|
|
50
52
|
"""List deliverables for current engagement."""
|
|
51
53
|
em = EngagementManager()
|
|
@@ -56,21 +58,21 @@ def list(category, status):
|
|
|
56
58
|
console.print("[red]No active engagement[/red]")
|
|
57
59
|
return
|
|
58
60
|
|
|
59
|
-
deliverables = dm.list_deliverables(
|
|
60
|
-
current['id'],
|
|
61
|
-
category=category,
|
|
62
|
-
status=status
|
|
63
|
-
)
|
|
61
|
+
deliverables = dm.list_deliverables(current["id"], category=category, status=status)
|
|
64
62
|
|
|
65
63
|
if not deliverables:
|
|
66
64
|
console.print("[yellow]No deliverables found[/yellow]")
|
|
67
|
-
console.print(
|
|
65
|
+
console.print(
|
|
66
|
+
"\nTip: Run 'souleyez deliverables init --defaults' to create default deliverables"
|
|
67
|
+
)
|
|
68
68
|
return
|
|
69
69
|
|
|
70
|
-
summary = dm.get_summary(current[
|
|
70
|
+
summary = dm.get_summary(current["id"])
|
|
71
71
|
|
|
72
72
|
console.print(f"\n[bold cyan]Deliverables - {current['name']}[/bold cyan]")
|
|
73
|
-
console.print(
|
|
73
|
+
console.print(
|
|
74
|
+
f"Completion: {summary['completed']}/{summary['total']} ({summary['completion_rate']*100:.0f}%)\n"
|
|
75
|
+
)
|
|
74
76
|
|
|
75
77
|
table = Table()
|
|
76
78
|
table.add_column("ID", style="cyan")
|
|
@@ -81,38 +83,33 @@ def list(category, status):
|
|
|
81
83
|
table.add_column("Priority", style="red")
|
|
82
84
|
|
|
83
85
|
for d in deliverables:
|
|
84
|
-
if d[
|
|
85
|
-
current_val = d[
|
|
86
|
-
target_val = d[
|
|
86
|
+
if d["target_type"] == "count":
|
|
87
|
+
current_val = d["current_value"] or 0
|
|
88
|
+
target_val = d["target_value"]
|
|
87
89
|
progress = f"{current_val}/{target_val}"
|
|
88
|
-
elif d[
|
|
89
|
-
progress = "✓" if d[
|
|
90
|
+
elif d["target_type"] == "boolean":
|
|
91
|
+
progress = "✓" if d["status"] == "completed" else "✗"
|
|
90
92
|
else:
|
|
91
93
|
progress = "Manual"
|
|
92
94
|
|
|
93
95
|
status_emoji = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
"completed": "✅",
|
|
97
|
+
"in_progress": "🔄",
|
|
98
|
+
"pending": "⚠️",
|
|
99
|
+
"failed": "❌",
|
|
98
100
|
}
|
|
99
101
|
status_str = f"{status_emoji.get(d['status'], '?')} {d['status']}"
|
|
100
102
|
|
|
101
103
|
priority_color = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
"critical": "[red]",
|
|
105
|
+
"high": "[yellow]",
|
|
106
|
+
"medium": "[blue]",
|
|
107
|
+
"low": "[dim]",
|
|
106
108
|
}
|
|
107
109
|
priority_str = f"{priority_color.get(d['priority'], '')}{ d['priority']}[/]"
|
|
108
110
|
|
|
109
111
|
table.add_row(
|
|
110
|
-
str(d[
|
|
111
|
-
d['category'],
|
|
112
|
-
d['title'],
|
|
113
|
-
progress,
|
|
114
|
-
status_str,
|
|
115
|
-
priority_str
|
|
112
|
+
str(d["id"]), d["category"], d["title"], progress, status_str, priority_str
|
|
116
113
|
)
|
|
117
114
|
|
|
118
115
|
console.print(table)
|
|
@@ -132,7 +129,7 @@ def validate():
|
|
|
132
129
|
|
|
133
130
|
console.print("[cyan]Validating deliverables...[/cyan]")
|
|
134
131
|
|
|
135
|
-
stats = dm.validate_all(current[
|
|
132
|
+
stats = dm.validate_all(current["id"])
|
|
136
133
|
|
|
137
134
|
console.print(f"[green]✓ Validated {stats['updated']} deliverables[/green]")
|
|
138
135
|
console.print(f" Completed: {stats['completed']}")
|
|
@@ -141,7 +138,7 @@ def validate():
|
|
|
141
138
|
|
|
142
139
|
|
|
143
140
|
@deliverables.command()
|
|
144
|
-
@click.argument(
|
|
141
|
+
@click.argument("deliverable_id", type=int)
|
|
145
142
|
def complete(deliverable_id):
|
|
146
143
|
"""Mark a deliverable as completed (manual deliverables)."""
|
|
147
144
|
dm = DeliverableManager()
|
|
@@ -157,11 +154,15 @@ def complete(deliverable_id):
|
|
|
157
154
|
|
|
158
155
|
|
|
159
156
|
@deliverables.command()
|
|
160
|
-
@click.argument(
|
|
161
|
-
@click.argument(
|
|
162
|
-
@click.option(
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
@click.argument("category")
|
|
158
|
+
@click.argument("title")
|
|
159
|
+
@click.option(
|
|
160
|
+
"--target-type", "-t", default="manual", help="Target type (count, boolean, manual)"
|
|
161
|
+
)
|
|
162
|
+
@click.option("--target-value", "-v", type=int, help="Target value (for count types)")
|
|
163
|
+
@click.option(
|
|
164
|
+
"--priority", "-p", default="medium", help="Priority (critical, high, medium, low)"
|
|
165
|
+
)
|
|
165
166
|
def add(category, title, target_type, target_value, priority):
|
|
166
167
|
"""Add a custom deliverable."""
|
|
167
168
|
em = EngagementManager()
|
|
@@ -173,12 +174,12 @@ def add(category, title, target_type, target_value, priority):
|
|
|
173
174
|
return
|
|
174
175
|
|
|
175
176
|
deliverable_id = dm.add_deliverable(
|
|
176
|
-
engagement_id=current[
|
|
177
|
+
engagement_id=current["id"],
|
|
177
178
|
category=category,
|
|
178
179
|
title=title,
|
|
179
180
|
target_type=target_type,
|
|
180
181
|
target_value=target_value,
|
|
181
|
-
priority=priority
|
|
182
|
+
priority=priority,
|
|
182
183
|
)
|
|
183
184
|
|
|
184
185
|
console.print(f"[green]✓ Deliverable added: ID {deliverable_id}[/green]")
|
|
@@ -186,8 +187,8 @@ def add(category, title, target_type, target_value, priority):
|
|
|
186
187
|
|
|
187
188
|
|
|
188
189
|
@deliverables.command()
|
|
189
|
-
@click.argument(
|
|
190
|
-
@click.confirmation_option(prompt=
|
|
190
|
+
@click.argument("deliverable_id", type=int)
|
|
191
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this deliverable?")
|
|
191
192
|
def delete(deliverable_id):
|
|
192
193
|
"""Delete a deliverable."""
|
|
193
194
|
dm = DeliverableManager()
|
|
@@ -209,11 +210,13 @@ def summary():
|
|
|
209
210
|
console.print("[red]No active engagement[/red]")
|
|
210
211
|
return
|
|
211
212
|
|
|
212
|
-
summary = dm.get_summary(current[
|
|
213
|
+
summary = dm.get_summary(current["id"])
|
|
213
214
|
|
|
214
215
|
console.print(f"\n[bold cyan]Deliverable Summary - {current['name']}[/bold cyan]\n")
|
|
215
216
|
|
|
216
|
-
console.print(
|
|
217
|
+
console.print(
|
|
218
|
+
f"[bold]Overall Progress:[/bold] {summary['completed']}/{summary['total']} ({summary['completion_rate']*100:.0f}%)"
|
|
219
|
+
)
|
|
217
220
|
|
|
218
221
|
columns = [
|
|
219
222
|
TextColumn("[bold blue]{task.description}"),
|
|
@@ -225,17 +228,19 @@ def summary():
|
|
|
225
228
|
|
|
226
229
|
with progress:
|
|
227
230
|
task = progress.add_task(
|
|
228
|
-
"Completion",
|
|
229
|
-
total=summary['total'],
|
|
230
|
-
completed=summary['completed']
|
|
231
|
+
"Completion", total=summary["total"], completed=summary["completed"]
|
|
231
232
|
)
|
|
232
233
|
|
|
233
234
|
console.print("\n[bold]By Category:[/bold]\n")
|
|
234
235
|
|
|
235
|
-
for category, stats in summary[
|
|
236
|
-
completion_rate =
|
|
236
|
+
for category, stats in summary["by_category"].items():
|
|
237
|
+
completion_rate = (
|
|
238
|
+
stats["completed"] / stats["total"] if stats["total"] > 0 else 0
|
|
239
|
+
)
|
|
237
240
|
|
|
238
|
-
console.print(
|
|
241
|
+
console.print(
|
|
242
|
+
f" {category.title()}: {stats['completed']}/{stats['total']} ({completion_rate*100:.0f}%)"
|
|
243
|
+
)
|
|
239
244
|
console.print(f" ✅ Completed: {stats['completed']}")
|
|
240
245
|
console.print(f" 🔄 In Progress: {stats['in_progress']}")
|
|
241
246
|
console.print(f" ⚠️ Pending: {stats['pending']}")
|
souleyez/commands/engagement.py
CHANGED
|
@@ -7,13 +7,17 @@ Commands:
|
|
|
7
7
|
- souleyez engagement team remove <name> <user> - Remove team member
|
|
8
8
|
- souleyez engagement team transfer <name> <user> - Transfer ownership
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import click
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.table import Table
|
|
13
14
|
|
|
14
15
|
from souleyez.security import require_login
|
|
15
16
|
from souleyez.auth import get_current_user, Role, UserManager
|
|
16
|
-
from souleyez.auth.engagement_access import
|
|
17
|
+
from souleyez.auth.engagement_access import (
|
|
18
|
+
EngagementAccessManager,
|
|
19
|
+
EngagementPermission,
|
|
20
|
+
)
|
|
17
21
|
from souleyez.storage.engagements import EngagementManager
|
|
18
22
|
from souleyez.storage.database import get_db
|
|
19
23
|
|
|
@@ -39,12 +43,12 @@ def team_list(engagement_name):
|
|
|
39
43
|
return
|
|
40
44
|
|
|
41
45
|
# Check access
|
|
42
|
-
if not em.can_access(eng[
|
|
46
|
+
if not em.can_access(eng["id"]):
|
|
43
47
|
console.print("[red]❌ You don't have access to this engagement[/red]")
|
|
44
48
|
return
|
|
45
49
|
|
|
46
50
|
access_mgr = EngagementAccessManager(get_db().db_path)
|
|
47
|
-
members = access_mgr.get_team_members(eng[
|
|
51
|
+
members = access_mgr.get_team_members(eng["id"])
|
|
48
52
|
|
|
49
53
|
if not members:
|
|
50
54
|
console.print(f"[yellow]No team members found for '{engagement_name}'[/yellow]")
|
|
@@ -57,17 +61,15 @@ def team_list(engagement_name):
|
|
|
57
61
|
table.add_column("Added")
|
|
58
62
|
|
|
59
63
|
for m in members:
|
|
60
|
-
role_style = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'viewer': 'dim'
|
|
64
|
-
}.get(m['permission_level'], 'white')
|
|
64
|
+
role_style = {"owner": "green bold", "editor": "cyan", "viewer": "dim"}.get(
|
|
65
|
+
m["permission_level"], "white"
|
|
66
|
+
)
|
|
65
67
|
|
|
66
68
|
table.add_row(
|
|
67
|
-
m[
|
|
68
|
-
m[
|
|
69
|
+
m["username"],
|
|
70
|
+
m["email"] or "-",
|
|
69
71
|
f"[{role_style}]{m['permission_level'].upper()}[/{role_style}]",
|
|
70
|
-
m[
|
|
72
|
+
m["granted_at"][:10] if m["granted_at"] else "-",
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
console.print(table)
|
|
@@ -78,10 +80,13 @@ def team_list(engagement_name):
|
|
|
78
80
|
@require_login
|
|
79
81
|
@click.argument("engagement_name")
|
|
80
82
|
@click.argument("username")
|
|
81
|
-
@click.option(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
@click.option(
|
|
84
|
+
"--role",
|
|
85
|
+
"-r",
|
|
86
|
+
type=click.Choice(["editor", "viewer"]),
|
|
87
|
+
default="viewer",
|
|
88
|
+
help="Permission level (default: viewer)",
|
|
89
|
+
)
|
|
85
90
|
def team_add(engagement_name, username, role):
|
|
86
91
|
"""Add a user to an engagement's team."""
|
|
87
92
|
em = EngagementManager()
|
|
@@ -95,8 +100,10 @@ def team_add(engagement_name, username, role):
|
|
|
95
100
|
access_mgr = EngagementAccessManager(get_db().db_path)
|
|
96
101
|
user = get_current_user()
|
|
97
102
|
|
|
98
|
-
if not access_mgr.can_manage_team(eng[
|
|
99
|
-
console.print(
|
|
103
|
+
if not access_mgr.can_manage_team(eng["id"], user.id, user.role):
|
|
104
|
+
console.print(
|
|
105
|
+
"[red]❌ Only the engagement owner or admin can manage team members[/red]"
|
|
106
|
+
)
|
|
100
107
|
return
|
|
101
108
|
|
|
102
109
|
# Find target user
|
|
@@ -108,11 +115,15 @@ def team_add(engagement_name, username, role):
|
|
|
108
115
|
return
|
|
109
116
|
|
|
110
117
|
# Add team member
|
|
111
|
-
perm =
|
|
112
|
-
|
|
118
|
+
perm = (
|
|
119
|
+
EngagementPermission.EDITOR if role == "editor" else EngagementPermission.VIEWER
|
|
120
|
+
)
|
|
121
|
+
success, error = access_mgr.add_team_member(eng["id"], target.id, perm, user.id)
|
|
113
122
|
|
|
114
123
|
if success:
|
|
115
|
-
console.print(
|
|
124
|
+
console.print(
|
|
125
|
+
f"[green]✅ Added {username} as {role} to '{engagement_name}'[/green]"
|
|
126
|
+
)
|
|
116
127
|
else:
|
|
117
128
|
console.print(f"[red]❌ Failed: {error}[/red]")
|
|
118
129
|
|
|
@@ -133,8 +144,10 @@ def team_remove(engagement_name, username):
|
|
|
133
144
|
access_mgr = EngagementAccessManager(get_db().db_path)
|
|
134
145
|
user = get_current_user()
|
|
135
146
|
|
|
136
|
-
if not access_mgr.can_manage_team(eng[
|
|
137
|
-
console.print(
|
|
147
|
+
if not access_mgr.can_manage_team(eng["id"], user.id, user.role):
|
|
148
|
+
console.print(
|
|
149
|
+
"[red]❌ Only the engagement owner or admin can manage team members[/red]"
|
|
150
|
+
)
|
|
138
151
|
return
|
|
139
152
|
|
|
140
153
|
user_mgr = UserManager(get_db().db_path)
|
|
@@ -144,7 +157,7 @@ def team_remove(engagement_name, username):
|
|
|
144
157
|
console.print(f"[red]❌ User '{username}' not found[/red]")
|
|
145
158
|
return
|
|
146
159
|
|
|
147
|
-
success, error = access_mgr.remove_team_member(eng[
|
|
160
|
+
success, error = access_mgr.remove_team_member(eng["id"], target.id)
|
|
148
161
|
|
|
149
162
|
if success:
|
|
150
163
|
console.print(f"[green]✅ Removed {username} from '{engagement_name}'[/green]")
|
|
@@ -170,8 +183,10 @@ def team_transfer(engagement_name, new_owner_username, force):
|
|
|
170
183
|
user = get_current_user()
|
|
171
184
|
|
|
172
185
|
# Only owner or admin can transfer
|
|
173
|
-
if not access_mgr.can_manage_team(eng[
|
|
174
|
-
console.print(
|
|
186
|
+
if not access_mgr.can_manage_team(eng["id"], user.id, user.role):
|
|
187
|
+
console.print(
|
|
188
|
+
"[red]❌ Only the engagement owner or admin can transfer ownership[/red]"
|
|
189
|
+
)
|
|
175
190
|
return
|
|
176
191
|
|
|
177
192
|
user_mgr = UserManager(get_db().db_path)
|
|
@@ -182,14 +197,18 @@ def team_transfer(engagement_name, new_owner_username, force):
|
|
|
182
197
|
return
|
|
183
198
|
|
|
184
199
|
if not force:
|
|
185
|
-
if not click.confirm(
|
|
200
|
+
if not click.confirm(
|
|
201
|
+
f"Transfer ownership of '{engagement_name}' to {new_owner_username}?"
|
|
202
|
+
):
|
|
186
203
|
console.print("[yellow]Cancelled[/yellow]")
|
|
187
204
|
return
|
|
188
205
|
|
|
189
|
-
success, error = access_mgr.transfer_ownership(eng[
|
|
206
|
+
success, error = access_mgr.transfer_ownership(eng["id"], new_owner.id, user.id)
|
|
190
207
|
|
|
191
208
|
if success:
|
|
192
|
-
console.print(
|
|
209
|
+
console.print(
|
|
210
|
+
f"[green]✅ Ownership transferred to {new_owner_username}[/green]"
|
|
211
|
+
)
|
|
193
212
|
console.print(f" You have been added as an editor.")
|
|
194
213
|
else:
|
|
195
214
|
console.print(f"[red]❌ Failed: {error}[/red]")
|
souleyez/commands/license.py
CHANGED
|
@@ -18,13 +18,13 @@ def license():
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@license.command()
|
|
21
|
-
@click.argument(
|
|
21
|
+
@click.argument("license_key")
|
|
22
22
|
def activate(license_key: str):
|
|
23
23
|
"""Activate a Pro license key."""
|
|
24
24
|
from souleyez.licensing import activate_license, validate_license
|
|
25
25
|
|
|
26
26
|
click.echo()
|
|
27
|
-
click.echo(click.style(" Activating license...", fg=
|
|
27
|
+
click.echo(click.style(" Activating license...", fg="cyan"))
|
|
28
28
|
|
|
29
29
|
# Validate and save
|
|
30
30
|
success, message = activate_license(license_key)
|
|
@@ -32,7 +32,9 @@ def activate(license_key: str):
|
|
|
32
32
|
if success:
|
|
33
33
|
info = validate_license(license_key)
|
|
34
34
|
click.echo()
|
|
35
|
-
click.echo(
|
|
35
|
+
click.echo(
|
|
36
|
+
click.style(" License activated successfully!", fg="green", bold=True)
|
|
37
|
+
)
|
|
36
38
|
click.echo()
|
|
37
39
|
click.echo(f" Email: {info.email}")
|
|
38
40
|
click.echo(f" Tier: {click.style(info.tier, fg='magenta', bold=True)}")
|
|
@@ -40,17 +42,19 @@ def activate(license_key: str):
|
|
|
40
42
|
if info.expires_at:
|
|
41
43
|
days = info.days_remaining
|
|
42
44
|
if days > 30:
|
|
43
|
-
color =
|
|
45
|
+
color = "green"
|
|
44
46
|
elif days > 7:
|
|
45
|
-
color =
|
|
47
|
+
color = "yellow"
|
|
46
48
|
else:
|
|
47
|
-
color =
|
|
48
|
-
click.echo(
|
|
49
|
+
color = "red"
|
|
50
|
+
click.echo(
|
|
51
|
+
f" Expires: {info.expires_at.strftime('%Y-%m-%d')} ({click.style(f'{days} days', fg=color)})"
|
|
52
|
+
)
|
|
49
53
|
else:
|
|
50
54
|
click.echo(f" Expires: {click.style('Never (perpetual)', fg='green')}")
|
|
51
55
|
|
|
52
56
|
click.echo()
|
|
53
|
-
click.echo(click.style(" Pro features are now unlocked!", fg=
|
|
57
|
+
click.echo(click.style(" Pro features are now unlocked!", fg="cyan"))
|
|
54
58
|
click.echo()
|
|
55
59
|
|
|
56
60
|
# Update user tier if auth system is in use
|
|
@@ -65,16 +69,17 @@ def activate(license_key: str):
|
|
|
65
69
|
user_mgr.set_user_tier(
|
|
66
70
|
user.id,
|
|
67
71
|
Tier.PRO,
|
|
68
|
-
license_key=license_key[:20]
|
|
72
|
+
license_key=license_key[:20]
|
|
73
|
+
+ "...", # Store truncated key as reference
|
|
69
74
|
expires_at=info.expires_at,
|
|
70
|
-
_bypass_validation=True # Already validated by licensing module
|
|
75
|
+
_bypass_validation=True, # Already validated by licensing module
|
|
71
76
|
)
|
|
72
77
|
except Exception:
|
|
73
78
|
pass # Auth system not in use
|
|
74
79
|
|
|
75
80
|
else:
|
|
76
81
|
click.echo()
|
|
77
|
-
click.echo(click.style(f" License activation failed: {message}", fg=
|
|
82
|
+
click.echo(click.style(f" License activation failed: {message}", fg="red"))
|
|
78
83
|
click.echo()
|
|
79
84
|
click.echo(" Please check your license key and try again.")
|
|
80
85
|
click.echo(" Contact support if the problem persists.")
|
|
@@ -90,7 +95,7 @@ def status():
|
|
|
90
95
|
info = get_active_license()
|
|
91
96
|
|
|
92
97
|
if info is None:
|
|
93
|
-
click.echo(click.style(" No active license", fg=
|
|
98
|
+
click.echo(click.style(" No active license", fg="yellow"))
|
|
94
99
|
click.echo()
|
|
95
100
|
click.echo(" You are using the FREE tier.")
|
|
96
101
|
click.echo(" Upgrade to Pro: https://www.cybersoulsecurity.com/souleyez")
|
|
@@ -100,14 +105,14 @@ def status():
|
|
|
100
105
|
return
|
|
101
106
|
|
|
102
107
|
if not info.is_valid:
|
|
103
|
-
click.echo(click.style(f" License invalid: {info.error}", fg=
|
|
108
|
+
click.echo(click.style(f" License invalid: {info.error}", fg="red"))
|
|
104
109
|
click.echo()
|
|
105
110
|
click.echo(" Please reactivate or contact support.")
|
|
106
111
|
click.echo()
|
|
107
112
|
return
|
|
108
113
|
|
|
109
114
|
# Valid license
|
|
110
|
-
click.echo(click.style(" License Status: ACTIVE", fg=
|
|
115
|
+
click.echo(click.style(" License Status: ACTIVE", fg="green", bold=True))
|
|
111
116
|
click.echo()
|
|
112
117
|
click.echo(f" Email: {info.email}")
|
|
113
118
|
click.echo(f" Tier: {click.style(info.tier, fg='magenta', bold=True)}")
|
|
@@ -115,12 +120,14 @@ def status():
|
|
|
115
120
|
if info.expires_at:
|
|
116
121
|
days = info.days_remaining
|
|
117
122
|
if days > 30:
|
|
118
|
-
color =
|
|
123
|
+
color = "green"
|
|
119
124
|
elif days > 7:
|
|
120
|
-
color =
|
|
125
|
+
color = "yellow"
|
|
121
126
|
else:
|
|
122
|
-
color =
|
|
123
|
-
click.echo(
|
|
127
|
+
color = "red"
|
|
128
|
+
click.echo(
|
|
129
|
+
f" Expires: {info.expires_at.strftime('%Y-%m-%d')} ({click.style(f'{days} days remaining', fg=color)})"
|
|
130
|
+
)
|
|
124
131
|
else:
|
|
125
132
|
click.echo(f" Expires: {click.style('Never (perpetual)', fg='green')}")
|
|
126
133
|
|
|
@@ -138,13 +145,15 @@ def deactivate():
|
|
|
138
145
|
info = get_active_license()
|
|
139
146
|
if info is None:
|
|
140
147
|
click.echo()
|
|
141
|
-
click.echo(click.style(" No active license to remove.", fg=
|
|
148
|
+
click.echo(click.style(" No active license to remove.", fg="yellow"))
|
|
142
149
|
click.echo()
|
|
143
150
|
return
|
|
144
151
|
|
|
145
152
|
# Confirm
|
|
146
153
|
click.echo()
|
|
147
|
-
click.echo(
|
|
154
|
+
click.echo(
|
|
155
|
+
click.style(" Warning: This will remove your Pro license.", fg="yellow")
|
|
156
|
+
)
|
|
148
157
|
click.echo(f" License: {info.email}")
|
|
149
158
|
click.echo()
|
|
150
159
|
|
|
@@ -154,7 +163,7 @@ def deactivate():
|
|
|
154
163
|
|
|
155
164
|
if deactivate_license():
|
|
156
165
|
click.echo()
|
|
157
|
-
click.echo(click.style(" License removed.", fg=
|
|
166
|
+
click.echo(click.style(" License removed.", fg="green"))
|
|
158
167
|
click.echo(" You are now on the FREE tier.")
|
|
159
168
|
click.echo()
|
|
160
169
|
|
|
@@ -171,11 +180,11 @@ def deactivate():
|
|
|
171
180
|
pass # Auth system not in use
|
|
172
181
|
else:
|
|
173
182
|
click.echo()
|
|
174
|
-
click.echo(click.style(" Failed to remove license.", fg=
|
|
183
|
+
click.echo(click.style(" Failed to remove license.", fg="red"))
|
|
175
184
|
click.echo()
|
|
176
185
|
|
|
177
186
|
|
|
178
|
-
@license.command(
|
|
187
|
+
@license.command("machine-id")
|
|
179
188
|
def machine_id():
|
|
180
189
|
"""Show this machine's ID for hardware-bound licenses."""
|
|
181
190
|
from souleyez.licensing.validator import get_machine_id
|