souleyez 2.22.0__py3-none-any.whl → 2.27.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.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- souleyez/__init__.py +1 -1
- souleyez/assets/__init__.py +1 -0
- souleyez/assets/souleyez-icon.png +0 -0
- souleyez/core/msf_sync_manager.py +15 -5
- souleyez/core/tool_chaining.py +126 -26
- souleyez/detection/validator.py +4 -2
- souleyez/docs/README.md +2 -2
- souleyez/docs/user-guide/configuration.md +1 -1
- souleyez/docs/user-guide/installation.md +14 -1
- souleyez/engine/background.py +620 -154
- souleyez/engine/result_handler.py +262 -1
- souleyez/engine/worker_manager.py +98 -2
- souleyez/main.py +103 -4
- souleyez/parsers/crackmapexec_parser.py +101 -43
- souleyez/parsers/dnsrecon_parser.py +50 -35
- souleyez/parsers/enum4linux_parser.py +101 -21
- souleyez/parsers/http_fingerprint_parser.py +319 -0
- souleyez/parsers/hydra_parser.py +56 -5
- souleyez/parsers/impacket_parser.py +123 -44
- souleyez/parsers/john_parser.py +47 -14
- souleyez/parsers/msf_parser.py +20 -5
- souleyez/parsers/nmap_parser.py +48 -27
- souleyez/parsers/smbmap_parser.py +39 -23
- souleyez/parsers/sqlmap_parser.py +18 -9
- souleyez/parsers/theharvester_parser.py +21 -13
- souleyez/plugins/http_fingerprint.py +598 -0
- souleyez/plugins/nuclei.py +41 -17
- souleyez/ui/interactive.py +99 -7
- souleyez/ui/setup_wizard.py +93 -5
- souleyez/ui/tool_setup.py +52 -52
- souleyez/utils/tool_checker.py +45 -5
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/METADATA +16 -3
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/RECORD +37 -33
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/WHEEL +0 -0
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.22.0.dist-info → souleyez-2.27.0.dist-info}/top_level.txt +0 -0
|
@@ -44,6 +44,7 @@ def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
44
44
|
return None
|
|
45
45
|
|
|
46
46
|
if not log_path or not os.path.exists(log_path):
|
|
47
|
+
logger.error(f"Job {job.get('id')} parse failed: log file missing or does not exist (path={log_path})")
|
|
47
48
|
return None
|
|
48
49
|
|
|
49
50
|
# Get engagement ID from job or fall back to current engagement
|
|
@@ -56,10 +57,12 @@ def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
56
57
|
engagement = em.get_current()
|
|
57
58
|
|
|
58
59
|
if not engagement:
|
|
60
|
+
logger.error(f"Job {job.get('id')} parse failed: no engagement_id and no current engagement")
|
|
59
61
|
return None
|
|
60
62
|
|
|
61
63
|
engagement_id = engagement['id']
|
|
62
|
-
except Exception:
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Job {job.get('id')} parse failed: engagement lookup error: {e}")
|
|
63
66
|
return None
|
|
64
67
|
|
|
65
68
|
# Route to appropriate parser
|
|
@@ -108,6 +111,15 @@ def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
108
111
|
parse_result = parse_nikto_job(engagement_id, log_path, job)
|
|
109
112
|
elif tool == 'dalfox':
|
|
110
113
|
parse_result = parse_dalfox_job(engagement_id, log_path, job)
|
|
114
|
+
elif tool == 'http_fingerprint':
|
|
115
|
+
parse_result = parse_http_fingerprint_job(engagement_id, log_path, job)
|
|
116
|
+
elif tool == 'hashcat':
|
|
117
|
+
parse_result = parse_hashcat_job(engagement_id, log_path, job)
|
|
118
|
+
elif tool == 'john':
|
|
119
|
+
parse_result = parse_john_job(engagement_id, log_path, job)
|
|
120
|
+
else:
|
|
121
|
+
# No parser for this tool - log it so we know
|
|
122
|
+
logger.warning(f"Job {job.get('id')} has no parser for tool '{tool}' - results not extracted")
|
|
111
123
|
|
|
112
124
|
# NOTE: Auto-chaining is now handled in background.py after parsing completes
|
|
113
125
|
# This avoids duplicate job creation and gives better control over timing
|
|
@@ -204,6 +216,8 @@ def reparse_job(job_id: int) -> Dict[str, Any]:
|
|
|
204
216
|
parse_result = parse_nikto_job(engagement_id, log_path, job)
|
|
205
217
|
elif tool == 'dalfox':
|
|
206
218
|
parse_result = parse_dalfox_job(engagement_id, log_path, job)
|
|
219
|
+
elif tool == 'http_fingerprint':
|
|
220
|
+
parse_result = parse_http_fingerprint_job(engagement_id, log_path, job)
|
|
207
221
|
else:
|
|
208
222
|
return {'success': False, 'message': f'No parser available for tool: {tool}'}
|
|
209
223
|
|
|
@@ -3068,6 +3082,91 @@ def parse_nikto_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> D
|
|
|
3068
3082
|
return {'error': str(e)}
|
|
3069
3083
|
|
|
3070
3084
|
|
|
3085
|
+
def parse_http_fingerprint_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
|
|
3086
|
+
"""
|
|
3087
|
+
Parse HTTP fingerprint results.
|
|
3088
|
+
|
|
3089
|
+
Returns fingerprint data for use in auto-chaining context.
|
|
3090
|
+
This enables downstream tools (nikto, nuclei, etc.) to make smarter decisions
|
|
3091
|
+
based on detected WAF, CDN, or managed hosting platform.
|
|
3092
|
+
"""
|
|
3093
|
+
try:
|
|
3094
|
+
from souleyez.parsers.http_fingerprint_parser import (
|
|
3095
|
+
parse_http_fingerprint_output,
|
|
3096
|
+
build_fingerprint_context,
|
|
3097
|
+
get_tool_recommendations
|
|
3098
|
+
)
|
|
3099
|
+
from souleyez.storage.hosts import HostManager
|
|
3100
|
+
from urllib.parse import urlparse
|
|
3101
|
+
|
|
3102
|
+
target = job.get('target', '')
|
|
3103
|
+
|
|
3104
|
+
# Read log file
|
|
3105
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
3106
|
+
output = f.read()
|
|
3107
|
+
|
|
3108
|
+
parsed = parse_http_fingerprint_output(output, target)
|
|
3109
|
+
|
|
3110
|
+
# Extract host from target URL
|
|
3111
|
+
parsed_url = urlparse(target)
|
|
3112
|
+
target_host = parsed_url.hostname or target
|
|
3113
|
+
|
|
3114
|
+
# Update host with fingerprint info if we have useful data
|
|
3115
|
+
if target_host and (parsed.get('server') or parsed.get('managed_hosting')):
|
|
3116
|
+
hm = HostManager()
|
|
3117
|
+
host_data = {
|
|
3118
|
+
'ip': target_host,
|
|
3119
|
+
'status': 'up'
|
|
3120
|
+
}
|
|
3121
|
+
# Store server info in notes or a dedicated field
|
|
3122
|
+
# For now, we just ensure the host exists
|
|
3123
|
+
hm.add_or_update_host(engagement_id, host_data)
|
|
3124
|
+
|
|
3125
|
+
# Build fingerprint context for chaining
|
|
3126
|
+
fingerprint_context = build_fingerprint_context(parsed)
|
|
3127
|
+
|
|
3128
|
+
# Get tool recommendations
|
|
3129
|
+
recommendations = get_tool_recommendations(parsed)
|
|
3130
|
+
|
|
3131
|
+
# Determine status
|
|
3132
|
+
if parsed.get('error'):
|
|
3133
|
+
status = STATUS_ERROR
|
|
3134
|
+
elif parsed.get('managed_hosting') or parsed.get('waf') or parsed.get('cdn'):
|
|
3135
|
+
status = STATUS_DONE # Found useful info
|
|
3136
|
+
elif parsed.get('server'):
|
|
3137
|
+
status = STATUS_DONE
|
|
3138
|
+
else:
|
|
3139
|
+
status = STATUS_NO_RESULTS
|
|
3140
|
+
|
|
3141
|
+
return {
|
|
3142
|
+
'tool': 'http_fingerprint',
|
|
3143
|
+
'status': status,
|
|
3144
|
+
'target': target,
|
|
3145
|
+
'target_host': target_host,
|
|
3146
|
+
# Core fingerprint data
|
|
3147
|
+
'server': parsed.get('server'),
|
|
3148
|
+
'managed_hosting': parsed.get('managed_hosting'),
|
|
3149
|
+
'waf': parsed.get('waf', []),
|
|
3150
|
+
'cdn': parsed.get('cdn', []),
|
|
3151
|
+
'technologies': parsed.get('technologies', []),
|
|
3152
|
+
'status_code': parsed.get('status_code'),
|
|
3153
|
+
# For auto-chaining context
|
|
3154
|
+
'http_fingerprint': fingerprint_context.get('http_fingerprint', {}),
|
|
3155
|
+
'recommendations': recommendations,
|
|
3156
|
+
# Pass through for downstream chains
|
|
3157
|
+
'services': [{
|
|
3158
|
+
'ip': target_host,
|
|
3159
|
+
'port': parsed_url.port or (443 if parsed_url.scheme == 'https' else 80),
|
|
3160
|
+
'service_name': 'https' if parsed_url.scheme == 'https' else 'http',
|
|
3161
|
+
'product': parsed.get('server', ''),
|
|
3162
|
+
}],
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
except Exception as e:
|
|
3166
|
+
logger.error(f"Error parsing http_fingerprint job: {e}")
|
|
3167
|
+
return {'error': str(e)}
|
|
3168
|
+
|
|
3169
|
+
|
|
3071
3170
|
def parse_dalfox_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
|
|
3072
3171
|
"""Parse Dalfox XSS scanner results."""
|
|
3073
3172
|
try:
|
|
@@ -3156,3 +3255,165 @@ def parse_dalfox_job(engagement_id: int, log_path: str, job: Dict[str, Any]) ->
|
|
|
3156
3255
|
except Exception as e:
|
|
3157
3256
|
logger.error(f"Error parsing dalfox job: {e}")
|
|
3158
3257
|
return {'error': str(e)}
|
|
3258
|
+
|
|
3259
|
+
|
|
3260
|
+
def parse_hashcat_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
|
|
3261
|
+
"""Parse hashcat job results and extract cracked passwords."""
|
|
3262
|
+
try:
|
|
3263
|
+
from souleyez.parsers.hashcat_parser import parse_hashcat_output, map_to_credentials
|
|
3264
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
3265
|
+
from souleyez.storage.findings import FindingsManager
|
|
3266
|
+
|
|
3267
|
+
# Read the log file
|
|
3268
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
3269
|
+
log_content = f.read()
|
|
3270
|
+
|
|
3271
|
+
# Parse hashcat output
|
|
3272
|
+
hash_file = job.get('metadata', {}).get('hash_file', '')
|
|
3273
|
+
parsed = parse_hashcat_output(log_content, hash_file)
|
|
3274
|
+
|
|
3275
|
+
# Store credentials
|
|
3276
|
+
cm = CredentialsManager()
|
|
3277
|
+
creds_added = 0
|
|
3278
|
+
|
|
3279
|
+
for cracked in parsed.get('cracked', []):
|
|
3280
|
+
try:
|
|
3281
|
+
cm.add_credential(
|
|
3282
|
+
engagement_id=engagement_id,
|
|
3283
|
+
host_id=None, # Hash cracking typically not tied to a specific host
|
|
3284
|
+
username='', # Hashcat doesn't always know the username
|
|
3285
|
+
password=cracked['password'],
|
|
3286
|
+
service='cracked_hash',
|
|
3287
|
+
credential_type='password',
|
|
3288
|
+
tool='hashcat',
|
|
3289
|
+
status='cracked',
|
|
3290
|
+
notes=f"Cracked from hash: {cracked['hash'][:32]}..."
|
|
3291
|
+
)
|
|
3292
|
+
creds_added += 1
|
|
3293
|
+
except Exception:
|
|
3294
|
+
pass # Skip duplicates
|
|
3295
|
+
|
|
3296
|
+
# Create finding if we cracked passwords
|
|
3297
|
+
fm = FindingsManager()
|
|
3298
|
+
findings_added = 0
|
|
3299
|
+
|
|
3300
|
+
if parsed.get('cracked'):
|
|
3301
|
+
fm.add_finding(
|
|
3302
|
+
engagement_id=engagement_id,
|
|
3303
|
+
title=f"Password Hashes Cracked - {len(parsed['cracked'])} passwords recovered",
|
|
3304
|
+
finding_type='credential',
|
|
3305
|
+
severity='high',
|
|
3306
|
+
description=f"Hashcat successfully cracked {len(parsed['cracked'])} password hash(es).\n\n"
|
|
3307
|
+
f"Status: {parsed['stats'].get('status', 'unknown')}\n"
|
|
3308
|
+
f"Cracked: {parsed['stats'].get('cracked_count', len(parsed['cracked']))}",
|
|
3309
|
+
tool='hashcat'
|
|
3310
|
+
)
|
|
3311
|
+
findings_added += 1
|
|
3312
|
+
|
|
3313
|
+
# Determine status
|
|
3314
|
+
if creds_added > 0:
|
|
3315
|
+
status = STATUS_DONE
|
|
3316
|
+
elif parsed['stats'].get('status') == 'exhausted':
|
|
3317
|
+
status = STATUS_NO_RESULTS # Ran to completion but found nothing
|
|
3318
|
+
else:
|
|
3319
|
+
status = STATUS_NO_RESULTS
|
|
3320
|
+
|
|
3321
|
+
return {
|
|
3322
|
+
'tool': 'hashcat',
|
|
3323
|
+
'status': status,
|
|
3324
|
+
'cracked_count': len(parsed.get('cracked', [])),
|
|
3325
|
+
'credentials_added': creds_added,
|
|
3326
|
+
'findings_added': findings_added,
|
|
3327
|
+
'hashcat_status': parsed['stats'].get('status', 'unknown')
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
except Exception as e:
|
|
3331
|
+
logger.error(f"Error parsing hashcat job: {e}")
|
|
3332
|
+
return {'error': str(e)}
|
|
3333
|
+
|
|
3334
|
+
|
|
3335
|
+
def parse_john_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
|
|
3336
|
+
"""Parse John the Ripper job results and extract cracked passwords."""
|
|
3337
|
+
try:
|
|
3338
|
+
from souleyez.parsers.john_parser import parse_john_output
|
|
3339
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
3340
|
+
from souleyez.storage.findings import FindingsManager
|
|
3341
|
+
|
|
3342
|
+
# Read the log file
|
|
3343
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
3344
|
+
log_content = f.read()
|
|
3345
|
+
|
|
3346
|
+
# Get hash file from job metadata if available
|
|
3347
|
+
hash_file = job.get('metadata', {}).get('hash_file', None)
|
|
3348
|
+
|
|
3349
|
+
# Parse john output
|
|
3350
|
+
parsed = parse_john_output(log_content, hash_file)
|
|
3351
|
+
|
|
3352
|
+
# Store credentials
|
|
3353
|
+
cm = CredentialsManager()
|
|
3354
|
+
creds_added = 0
|
|
3355
|
+
|
|
3356
|
+
for cred in parsed.get('cracked', []):
|
|
3357
|
+
username = cred.get('username', '')
|
|
3358
|
+
password = cred.get('password', '')
|
|
3359
|
+
|
|
3360
|
+
if password: # At minimum we need a password
|
|
3361
|
+
try:
|
|
3362
|
+
cm.add_credential(
|
|
3363
|
+
engagement_id=engagement_id,
|
|
3364
|
+
host_id=None, # Hash cracking typically not tied to a specific host
|
|
3365
|
+
username=username if username != 'unknown' else '',
|
|
3366
|
+
password=password,
|
|
3367
|
+
service='cracked_hash',
|
|
3368
|
+
credential_type='password',
|
|
3369
|
+
tool='john',
|
|
3370
|
+
status='cracked',
|
|
3371
|
+
notes=f"Cracked by John the Ripper"
|
|
3372
|
+
)
|
|
3373
|
+
creds_added += 1
|
|
3374
|
+
except Exception:
|
|
3375
|
+
pass # Skip duplicates
|
|
3376
|
+
|
|
3377
|
+
# Create finding if we cracked passwords
|
|
3378
|
+
fm = FindingsManager()
|
|
3379
|
+
findings_added = 0
|
|
3380
|
+
|
|
3381
|
+
if parsed.get('cracked'):
|
|
3382
|
+
usernames = [c.get('username', 'unknown') for c in parsed['cracked'] if c.get('username')]
|
|
3383
|
+
usernames_str = ', '.join(usernames[:10]) # First 10
|
|
3384
|
+
if len(usernames) > 10:
|
|
3385
|
+
usernames_str += f" (+{len(usernames) - 10} more)"
|
|
3386
|
+
|
|
3387
|
+
fm.add_finding(
|
|
3388
|
+
engagement_id=engagement_id,
|
|
3389
|
+
title=f"Password Hashes Cracked - {len(parsed['cracked'])} passwords recovered",
|
|
3390
|
+
finding_type='credential',
|
|
3391
|
+
severity='high',
|
|
3392
|
+
description=f"John the Ripper successfully cracked {len(parsed['cracked'])} password hash(es).\n\n"
|
|
3393
|
+
f"Usernames: {usernames_str}\n"
|
|
3394
|
+
f"Session status: {parsed.get('session_status', 'unknown')}",
|
|
3395
|
+
tool='john'
|
|
3396
|
+
)
|
|
3397
|
+
findings_added += 1
|
|
3398
|
+
|
|
3399
|
+
# Determine status
|
|
3400
|
+
if creds_added > 0:
|
|
3401
|
+
status = STATUS_DONE
|
|
3402
|
+
elif parsed.get('session_status') == 'completed':
|
|
3403
|
+
status = STATUS_NO_RESULTS # Ran to completion but found nothing
|
|
3404
|
+
else:
|
|
3405
|
+
status = STATUS_NO_RESULTS
|
|
3406
|
+
|
|
3407
|
+
return {
|
|
3408
|
+
'tool': 'john',
|
|
3409
|
+
'status': status,
|
|
3410
|
+
'cracked_count': len(parsed.get('cracked', [])),
|
|
3411
|
+
'credentials_added': creds_added,
|
|
3412
|
+
'findings_added': findings_added,
|
|
3413
|
+
'session_status': parsed.get('session_status', 'unknown'),
|
|
3414
|
+
'total_loaded': parsed.get('total_loaded', 0)
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
except Exception as e:
|
|
3418
|
+
logger.error(f"Error parsing john job: {e}")
|
|
3419
|
+
return {'error': str(e)}
|
|
@@ -4,12 +4,13 @@ Worker health check and management utilities
|
|
|
4
4
|
"""
|
|
5
5
|
import psutil
|
|
6
6
|
import time
|
|
7
|
-
from typing import Optional, Tuple
|
|
7
|
+
from typing import Optional, Tuple, Dict, Any
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def is_worker_running() -> Tuple[bool, Optional[int]]:
|
|
11
11
|
"""
|
|
12
|
-
Check if background worker is running
|
|
12
|
+
Check if background worker is running.
|
|
13
|
+
|
|
13
14
|
Returns: (is_running, pid)
|
|
14
15
|
"""
|
|
15
16
|
for proc in psutil.process_iter(['pid', 'cmdline']):
|
|
@@ -29,6 +30,40 @@ def is_worker_running() -> Tuple[bool, Optional[int]]:
|
|
|
29
30
|
return False, None
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def is_worker_healthy() -> Tuple[bool, Optional[int], Optional[str]]:
|
|
34
|
+
"""
|
|
35
|
+
Check if background worker is running AND healthy (responding).
|
|
36
|
+
|
|
37
|
+
Uses heartbeat file to verify worker is actively processing.
|
|
38
|
+
A worker process may exist but be frozen/hung - heartbeat detects this.
|
|
39
|
+
|
|
40
|
+
Returns: (is_healthy, pid, issue)
|
|
41
|
+
- is_healthy: True if worker is running and heartbeat is fresh
|
|
42
|
+
- pid: Worker PID if found, None otherwise
|
|
43
|
+
- issue: Description of issue if not healthy, None otherwise
|
|
44
|
+
"""
|
|
45
|
+
from souleyez.engine.background import (
|
|
46
|
+
get_heartbeat_age, HEARTBEAT_STALE_THRESHOLD
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
is_running, pid = is_worker_running()
|
|
50
|
+
|
|
51
|
+
if not is_running:
|
|
52
|
+
return False, None, "Worker process not found"
|
|
53
|
+
|
|
54
|
+
# Check heartbeat
|
|
55
|
+
heartbeat_age = get_heartbeat_age()
|
|
56
|
+
|
|
57
|
+
if heartbeat_age is None:
|
|
58
|
+
# No heartbeat file - worker may have just started
|
|
59
|
+
return True, pid, "No heartbeat yet (may be starting)"
|
|
60
|
+
|
|
61
|
+
if heartbeat_age > HEARTBEAT_STALE_THRESHOLD:
|
|
62
|
+
return False, pid, f"Heartbeat stale ({int(heartbeat_age)}s old, threshold: {HEARTBEAT_STALE_THRESHOLD}s)"
|
|
63
|
+
|
|
64
|
+
return True, pid, None
|
|
65
|
+
|
|
66
|
+
|
|
32
67
|
def start_worker_if_needed() -> bool:
|
|
33
68
|
"""
|
|
34
69
|
Start worker if not running
|
|
@@ -107,3 +142,64 @@ def get_worker_status() -> dict:
|
|
|
107
142
|
pass
|
|
108
143
|
|
|
109
144
|
return status
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_worker_health() -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Get detailed worker health status including heartbeat info.
|
|
150
|
+
|
|
151
|
+
Returns dict with:
|
|
152
|
+
- running: Whether worker process exists
|
|
153
|
+
- healthy: Whether worker is running AND responsive
|
|
154
|
+
- pid: Worker PID if running
|
|
155
|
+
- uptime: Seconds since worker started
|
|
156
|
+
- heartbeat_age: Seconds since last heartbeat
|
|
157
|
+
- heartbeat_stale: Whether heartbeat is stale
|
|
158
|
+
- issue: Description of any health issue
|
|
159
|
+
- cpu_percent: CPU usage percentage
|
|
160
|
+
- memory_mb: Memory usage in MB
|
|
161
|
+
"""
|
|
162
|
+
from souleyez.engine.background import (
|
|
163
|
+
get_heartbeat_age, HEARTBEAT_STALE_THRESHOLD
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
is_running, pid = is_worker_running()
|
|
167
|
+
heartbeat_age = get_heartbeat_age()
|
|
168
|
+
|
|
169
|
+
health = {
|
|
170
|
+
'running': is_running,
|
|
171
|
+
'healthy': False,
|
|
172
|
+
'pid': pid,
|
|
173
|
+
'uptime': None,
|
|
174
|
+
'heartbeat_age': heartbeat_age,
|
|
175
|
+
'heartbeat_stale': heartbeat_age is None or heartbeat_age > HEARTBEAT_STALE_THRESHOLD,
|
|
176
|
+
'issue': None,
|
|
177
|
+
'cpu_percent': None,
|
|
178
|
+
'memory_mb': None
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if not is_running:
|
|
182
|
+
health['issue'] = "Worker process not found"
|
|
183
|
+
return health
|
|
184
|
+
|
|
185
|
+
# Get process info
|
|
186
|
+
try:
|
|
187
|
+
proc = psutil.Process(pid)
|
|
188
|
+
health['uptime'] = int(time.time() - proc.create_time())
|
|
189
|
+
health['cpu_percent'] = proc.cpu_percent(interval=0.1)
|
|
190
|
+
health['memory_mb'] = round(proc.memory_info().rss / 1024 / 1024, 1)
|
|
191
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
192
|
+
health['issue'] = "Cannot access worker process"
|
|
193
|
+
return health
|
|
194
|
+
|
|
195
|
+
# Check heartbeat
|
|
196
|
+
if heartbeat_age is None:
|
|
197
|
+
health['issue'] = "No heartbeat yet (worker may be starting)"
|
|
198
|
+
health['healthy'] = True # Give benefit of doubt for new workers
|
|
199
|
+
elif heartbeat_age > HEARTBEAT_STALE_THRESHOLD:
|
|
200
|
+
health['issue'] = f"Worker unresponsive (heartbeat {int(heartbeat_age)}s old)"
|
|
201
|
+
health['healthy'] = False
|
|
202
|
+
else:
|
|
203
|
+
health['healthy'] = True
|
|
204
|
+
|
|
205
|
+
return health
|
souleyez/main.py
CHANGED
|
@@ -173,7 +173,7 @@ def _check_privileged_tools():
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
@click.group()
|
|
176
|
-
@click.version_option(version='2.
|
|
176
|
+
@click.version_option(version='2.27.0')
|
|
177
177
|
def cli():
|
|
178
178
|
"""SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
|
|
179
179
|
from souleyez.log_config import init_logging
|
|
@@ -1388,19 +1388,24 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1388
1388
|
path_dirs = os.environ.get('PATH', '').split(':')
|
|
1389
1389
|
pipx_bin = str(Path.home() / '.local' / 'bin')
|
|
1390
1390
|
go_bin = str(Path.home() / 'go' / 'bin')
|
|
1391
|
+
|
|
1392
|
+
# Detect shell config file (zsh for Kali, bash for others)
|
|
1393
|
+
shell = os.environ.get('SHELL', '/bin/bash')
|
|
1394
|
+
shell_rc = '~/.zshrc' if 'zsh' in shell else '~/.bashrc'
|
|
1395
|
+
|
|
1391
1396
|
if pipx_bin in path_dirs:
|
|
1392
1397
|
if verbose:
|
|
1393
1398
|
check_pass("PATH includes ~/.local/bin (pipx)")
|
|
1394
1399
|
else:
|
|
1395
1400
|
if Path(pipx_bin).exists() and any(Path(pipx_bin).iterdir()):
|
|
1396
|
-
check_warn("~/.local/bin not in PATH", "Add to
|
|
1401
|
+
check_warn("~/.local/bin not in PATH", f"Add to {shell_rc}: export PATH=\"$HOME/.local/bin:$PATH\"")
|
|
1397
1402
|
|
|
1398
1403
|
if go_bin in path_dirs:
|
|
1399
1404
|
if verbose:
|
|
1400
1405
|
check_pass("PATH includes ~/go/bin")
|
|
1401
1406
|
else:
|
|
1402
1407
|
if Path(go_bin).exists() and any(Path(go_bin).iterdir()):
|
|
1403
|
-
check_warn("~/go/bin not in PATH", "Add to
|
|
1408
|
+
check_warn("~/go/bin not in PATH", f"Add to {shell_rc}: export PATH=\"$HOME/go/bin:$PATH\"")
|
|
1404
1409
|
|
|
1405
1410
|
# Check database is writable
|
|
1406
1411
|
if db_path.exists():
|
|
@@ -1430,8 +1435,10 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1430
1435
|
# Section 7: MSF Database (if msfconsole available)
|
|
1431
1436
|
if shutil.which('msfconsole'):
|
|
1432
1437
|
click.echo(click.style("Metasploit", bold=True))
|
|
1438
|
+
# Check user config first, then system-wide config (Kali uses system-wide)
|
|
1433
1439
|
msf_db = Path.home() / '.msf4' / 'database.yml'
|
|
1434
|
-
|
|
1440
|
+
system_msf_db = Path('/usr/share/metasploit-framework/config/database.yml')
|
|
1441
|
+
if msf_db.exists() or system_msf_db.exists():
|
|
1435
1442
|
check_pass("MSF database configured")
|
|
1436
1443
|
else:
|
|
1437
1444
|
check_fail("MSF database not initialized", "msfdb init")
|
|
@@ -1513,6 +1520,98 @@ def tutorial():
|
|
|
1513
1520
|
run_tutorial()
|
|
1514
1521
|
|
|
1515
1522
|
|
|
1523
|
+
@cli.command('install-desktop')
|
|
1524
|
+
@click.option('--remove', is_flag=True, help='Remove the desktop shortcut')
|
|
1525
|
+
def install_desktop(remove):
|
|
1526
|
+
"""Install SoulEyez desktop shortcut in Applications menu.
|
|
1527
|
+
|
|
1528
|
+
Creates a .desktop file so SoulEyez appears in your
|
|
1529
|
+
Applications > Security menu with its icon.
|
|
1530
|
+
"""
|
|
1531
|
+
import shutil
|
|
1532
|
+
from importlib import resources
|
|
1533
|
+
|
|
1534
|
+
applications_dir = Path.home() / '.local' / 'share' / 'applications'
|
|
1535
|
+
icons_dir = Path.home() / '.local' / 'share' / 'icons'
|
|
1536
|
+
desktop_file = applications_dir / 'souleyez.desktop'
|
|
1537
|
+
icon_dest = icons_dir / 'souleyez.png'
|
|
1538
|
+
|
|
1539
|
+
if remove:
|
|
1540
|
+
# Remove desktop shortcut
|
|
1541
|
+
removed = False
|
|
1542
|
+
if desktop_file.exists():
|
|
1543
|
+
desktop_file.unlink()
|
|
1544
|
+
click.echo(click.style(" Removed desktop shortcut", fg='green'))
|
|
1545
|
+
removed = True
|
|
1546
|
+
if icon_dest.exists():
|
|
1547
|
+
icon_dest.unlink()
|
|
1548
|
+
click.echo(click.style(" Removed icon", fg='green'))
|
|
1549
|
+
removed = True
|
|
1550
|
+
if removed:
|
|
1551
|
+
click.echo(click.style("\nSoulEyez removed from Applications menu.", fg='cyan'))
|
|
1552
|
+
else:
|
|
1553
|
+
click.echo(click.style("No desktop shortcut found.", fg='yellow'))
|
|
1554
|
+
return
|
|
1555
|
+
|
|
1556
|
+
click.echo(click.style("\nInstalling SoulEyez desktop shortcut...\n", fg='cyan', bold=True))
|
|
1557
|
+
|
|
1558
|
+
# Create directories
|
|
1559
|
+
applications_dir.mkdir(parents=True, exist_ok=True)
|
|
1560
|
+
icons_dir.mkdir(parents=True, exist_ok=True)
|
|
1561
|
+
|
|
1562
|
+
# Find and copy icon
|
|
1563
|
+
try:
|
|
1564
|
+
# Try importlib.resources first (Python 3.9+)
|
|
1565
|
+
try:
|
|
1566
|
+
from importlib.resources import files
|
|
1567
|
+
icon_source = files('souleyez.assets').joinpath('souleyez-icon.png')
|
|
1568
|
+
with open(icon_source, 'rb') as src:
|
|
1569
|
+
icon_data = src.read()
|
|
1570
|
+
except (ImportError, TypeError, FileNotFoundError):
|
|
1571
|
+
# Fallback: find icon relative to this file
|
|
1572
|
+
icon_source = Path(__file__).parent / 'assets' / 'souleyez-icon.png'
|
|
1573
|
+
with open(icon_source, 'rb') as src:
|
|
1574
|
+
icon_data = src.read()
|
|
1575
|
+
|
|
1576
|
+
with open(icon_dest, 'wb') as dst:
|
|
1577
|
+
dst.write(icon_data)
|
|
1578
|
+
click.echo(click.style(" Installed icon", fg='green'))
|
|
1579
|
+
except Exception as e:
|
|
1580
|
+
click.echo(click.style(f" Warning: Could not copy icon: {e}", fg='yellow'))
|
|
1581
|
+
icon_dest = "utilities-terminal" # Fallback to system icon
|
|
1582
|
+
|
|
1583
|
+
# Create .desktop file
|
|
1584
|
+
desktop_content = f"""[Desktop Entry]
|
|
1585
|
+
Name=SoulEyez
|
|
1586
|
+
Comment=AI-Powered Penetration Testing Platform
|
|
1587
|
+
Exec=souleyez interactive
|
|
1588
|
+
Icon={icon_dest}
|
|
1589
|
+
Terminal=true
|
|
1590
|
+
Type=Application
|
|
1591
|
+
Categories=Security;System;Network;
|
|
1592
|
+
Keywords=pentest;security;hacking;nmap;metasploit;
|
|
1593
|
+
"""
|
|
1594
|
+
|
|
1595
|
+
desktop_file.write_text(desktop_content)
|
|
1596
|
+
click.echo(click.style(" Created desktop entry", fg='green'))
|
|
1597
|
+
|
|
1598
|
+
# Update desktop database (optional, may not be available)
|
|
1599
|
+
try:
|
|
1600
|
+
import subprocess
|
|
1601
|
+
subprocess.run(['update-desktop-database', str(applications_dir)],
|
|
1602
|
+
capture_output=True, check=False)
|
|
1603
|
+
except Exception:
|
|
1604
|
+
pass # Not critical if this fails
|
|
1605
|
+
|
|
1606
|
+
click.echo()
|
|
1607
|
+
click.echo(click.style("SoulEyez added to Applications menu!", fg='green', bold=True))
|
|
1608
|
+
click.echo()
|
|
1609
|
+
click.echo("You can find it under:")
|
|
1610
|
+
click.echo(click.style(" Applications > Security > SoulEyez", fg='cyan'))
|
|
1611
|
+
click.echo()
|
|
1612
|
+
click.echo("To remove: souleyez install-desktop --remove")
|
|
1613
|
+
|
|
1614
|
+
|
|
1516
1615
|
def main():
|
|
1517
1616
|
"""Main entry point."""
|
|
1518
1617
|
cli()
|