souleyez 2.27.0__py3-none-any.whl → 2.28.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 -1
- souleyez/core/tool_chaining.py +36 -12
- souleyez/docs/README.md +2 -2
- souleyez/docs/user-guide/configuration.md +1 -1
- souleyez/docs/user-guide/scope-management.md +683 -0
- souleyez/engine/background.py +38 -1
- souleyez/engine/result_handler.py +167 -10
- souleyez/main.py +222 -1
- souleyez/plugins/nuclei.py +2 -1
- souleyez/plugins/searchsploit.py +21 -18
- souleyez/security/scope_validator.py +615 -0
- souleyez/storage/hosts.py +87 -2
- souleyez/storage/migrations/_026_add_engagement_scope.py +87 -0
- souleyez/ui/interactive.py +289 -5
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/METADATA +9 -3
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/RECORD +20 -17
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/WHEEL +0 -0
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.27.0.dist-info → souleyez-2.28.0.dist-info}/top_level.txt +0 -0
souleyez/engine/background.py
CHANGED
|
@@ -321,7 +321,7 @@ def _next_job_id(jobs: List[Dict[str, Any]]) -> int:
|
|
|
321
321
|
return maxid + 1
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
def enqueue_job(tool: str, target: str, args: List[str], label: str = "", engagement_id: int = None, metadata: Dict[str, Any] = None, parent_id: int = None, reason: str = None, rule_id: int = None) -> int:
|
|
324
|
+
def enqueue_job(tool: str, target: str, args: List[str], label: str = "", engagement_id: int = None, metadata: Dict[str, Any] = None, parent_id: int = None, reason: str = None, rule_id: int = None, skip_scope_check: bool = False) -> int:
|
|
325
325
|
with _lock:
|
|
326
326
|
jobs = _read_jobs()
|
|
327
327
|
jid = _next_job_id(jobs)
|
|
@@ -339,6 +339,43 @@ def enqueue_job(tool: str, target: str, args: List[str], label: str = "", engage
|
|
|
339
339
|
|
|
340
340
|
# Merge parent_id, reason, and rule_id into metadata
|
|
341
341
|
job_metadata = metadata or {}
|
|
342
|
+
|
|
343
|
+
# Scope validation - check if target is within engagement scope
|
|
344
|
+
if not skip_scope_check and engagement_id:
|
|
345
|
+
try:
|
|
346
|
+
from souleyez.security.scope_validator import ScopeValidator, ScopeViolationError
|
|
347
|
+
validator = ScopeValidator(engagement_id)
|
|
348
|
+
result = validator.validate_target(target)
|
|
349
|
+
enforcement = validator.get_enforcement_mode()
|
|
350
|
+
|
|
351
|
+
if not result.is_in_scope and validator.has_scope_defined():
|
|
352
|
+
if enforcement == 'block':
|
|
353
|
+
validator.log_validation(target, result, 'blocked', job_id=jid)
|
|
354
|
+
raise ScopeViolationError(
|
|
355
|
+
f"Target '{target}' is out of scope. {result.reason}"
|
|
356
|
+
)
|
|
357
|
+
elif enforcement == 'warn':
|
|
358
|
+
validator.log_validation(target, result, 'warned', job_id=jid)
|
|
359
|
+
if 'warnings' not in job_metadata:
|
|
360
|
+
job_metadata['warnings'] = []
|
|
361
|
+
job_metadata['warnings'].append(
|
|
362
|
+
f"SCOPE WARNING: {target} may be out of scope. {result.reason}"
|
|
363
|
+
)
|
|
364
|
+
logger.warning("Out-of-scope target allowed (warn mode)", extra={
|
|
365
|
+
"target": target,
|
|
366
|
+
"engagement_id": engagement_id,
|
|
367
|
+
"reason": result.reason
|
|
368
|
+
})
|
|
369
|
+
else:
|
|
370
|
+
validator.log_validation(target, result, 'allowed', job_id=jid)
|
|
371
|
+
except ScopeViolationError:
|
|
372
|
+
raise # Re-raise scope violations
|
|
373
|
+
except Exception as e:
|
|
374
|
+
# Don't block jobs if scope validation fails unexpectedly
|
|
375
|
+
logger.warning("Scope validation error (allowing job)", extra={
|
|
376
|
+
"target": target,
|
|
377
|
+
"error": str(e)
|
|
378
|
+
})
|
|
342
379
|
if parent_id is not None:
|
|
343
380
|
job_metadata['parent_id'] = parent_id
|
|
344
381
|
if reason:
|
|
@@ -10,6 +10,95 @@ from .job_status import STATUS_DONE, STATUS_NO_RESULTS, STATUS_WARNING, STATUS_E
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
# Common error patterns that indicate tool failure (not "no results")
|
|
14
|
+
TOOL_ERROR_PATTERNS = {
|
|
15
|
+
'common': [
|
|
16
|
+
'connection refused',
|
|
17
|
+
'connection timed out',
|
|
18
|
+
'no route to host',
|
|
19
|
+
'network is unreachable',
|
|
20
|
+
'name or service not known',
|
|
21
|
+
'temporary failure in name resolution',
|
|
22
|
+
'host is down',
|
|
23
|
+
'connection reset by peer',
|
|
24
|
+
],
|
|
25
|
+
'nmap': [
|
|
26
|
+
'host seems down',
|
|
27
|
+
'note: host seems down',
|
|
28
|
+
'failed to resolve',
|
|
29
|
+
],
|
|
30
|
+
'gobuster': [
|
|
31
|
+
'timeout occurred during the request',
|
|
32
|
+
'error on running gobuster',
|
|
33
|
+
'unable to connect',
|
|
34
|
+
'context deadline exceeded',
|
|
35
|
+
],
|
|
36
|
+
'hydra': [
|
|
37
|
+
'can not connect',
|
|
38
|
+
'could not connect',
|
|
39
|
+
'error connecting',
|
|
40
|
+
'target does not support',
|
|
41
|
+
],
|
|
42
|
+
'nikto': [
|
|
43
|
+
'error connecting to host',
|
|
44
|
+
'unable to connect',
|
|
45
|
+
'no web server found',
|
|
46
|
+
],
|
|
47
|
+
'nuclei': [
|
|
48
|
+
'could not connect',
|
|
49
|
+
'context deadline exceeded',
|
|
50
|
+
'no address found',
|
|
51
|
+
],
|
|
52
|
+
'ffuf': [
|
|
53
|
+
'error making request',
|
|
54
|
+
'context deadline exceeded',
|
|
55
|
+
],
|
|
56
|
+
'sqlmap': [
|
|
57
|
+
'connection timed out',
|
|
58
|
+
'unable to connect',
|
|
59
|
+
'target url content is not stable',
|
|
60
|
+
],
|
|
61
|
+
'enum4linux': [
|
|
62
|
+
'could not initialise',
|
|
63
|
+
'nt_status_connection_refused',
|
|
64
|
+
'nt_status_host_unreachable',
|
|
65
|
+
'nt_status_io_timeout',
|
|
66
|
+
],
|
|
67
|
+
'smbmap': [
|
|
68
|
+
'could not connect',
|
|
69
|
+
'connection error',
|
|
70
|
+
'nt_status_connection_refused',
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def detect_tool_error(log_content: str, tool: str) -> Optional[str]:
|
|
76
|
+
"""
|
|
77
|
+
Check log content for tool errors that indicate failure (not just "no results").
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
log_content: The log file content
|
|
81
|
+
tool: Tool name (lowercase)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Error pattern found, or None if no error detected
|
|
85
|
+
"""
|
|
86
|
+
log_lower = log_content.lower()
|
|
87
|
+
|
|
88
|
+
# Check common patterns
|
|
89
|
+
for pattern in TOOL_ERROR_PATTERNS['common']:
|
|
90
|
+
if pattern in log_lower:
|
|
91
|
+
return pattern
|
|
92
|
+
|
|
93
|
+
# Check tool-specific patterns
|
|
94
|
+
tool_patterns = TOOL_ERROR_PATTERNS.get(tool, [])
|
|
95
|
+
for pattern in tool_patterns:
|
|
96
|
+
if pattern in log_lower:
|
|
97
|
+
return pattern
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
13
102
|
def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
14
103
|
"""
|
|
15
104
|
Process completed job and parse results into database.
|
|
@@ -527,9 +616,16 @@ def parse_nmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Di
|
|
|
527
616
|
'version': svc.get('version', '')
|
|
528
617
|
})
|
|
529
618
|
|
|
619
|
+
# Check for nmap errors before determining status
|
|
620
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
621
|
+
log_content = f.read()
|
|
622
|
+
nmap_error = detect_tool_error(log_content, 'nmap')
|
|
623
|
+
|
|
530
624
|
# Determine status based on results
|
|
531
625
|
hosts_up = len([h for h in parsed.get('hosts', []) if h.get('status') == 'up'])
|
|
532
|
-
if
|
|
626
|
+
if nmap_error:
|
|
627
|
+
status = STATUS_ERROR # Tool failed to run properly
|
|
628
|
+
elif hosts_up > 0:
|
|
533
629
|
status = STATUS_DONE # Found hosts
|
|
534
630
|
else:
|
|
535
631
|
status = STATUS_NO_RESULTS # No hosts up
|
|
@@ -1122,8 +1218,13 @@ def parse_gobuster_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -
|
|
|
1122
1218
|
exclude_length = length_match.group(1)
|
|
1123
1219
|
logger.info(f"Gobuster wildcard detected: Length {exclude_length}b")
|
|
1124
1220
|
|
|
1221
|
+
# Check for gobuster errors
|
|
1222
|
+
gobuster_error = detect_tool_error(log_content, 'gobuster')
|
|
1223
|
+
|
|
1125
1224
|
# Determine status based on results
|
|
1126
|
-
if
|
|
1225
|
+
if gobuster_error:
|
|
1226
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
1227
|
+
elif wildcard_detected:
|
|
1127
1228
|
# Wildcard detected - warning status (triggers auto-retry)
|
|
1128
1229
|
status = STATUS_WARNING
|
|
1129
1230
|
elif stats['total'] > 0:
|
|
@@ -1504,8 +1605,13 @@ def parse_sqlmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) ->
|
|
|
1504
1605
|
|
|
1505
1606
|
stats = get_sqli_stats(parsed)
|
|
1506
1607
|
|
|
1608
|
+
# Check for sqlmap errors
|
|
1609
|
+
sqlmap_error = detect_tool_error(log_content, 'sqlmap')
|
|
1610
|
+
|
|
1507
1611
|
# Determine status based on results
|
|
1508
|
-
if
|
|
1612
|
+
if sqlmap_error:
|
|
1613
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
1614
|
+
elif stats['sqli_confirmed'] or stats['xss_possible'] or stats['fi_possible']:
|
|
1509
1615
|
status = STATUS_DONE # Found injection vulnerabilities
|
|
1510
1616
|
else:
|
|
1511
1617
|
status = STATUS_NO_RESULTS # No injections found
|
|
@@ -2011,11 +2117,22 @@ def parse_smbmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) ->
|
|
|
2011
2117
|
)
|
|
2012
2118
|
findings_added += 1
|
|
2013
2119
|
|
|
2120
|
+
# Check for smbmap errors
|
|
2121
|
+
smbmap_error = detect_tool_error(log_content, 'smbmap')
|
|
2122
|
+
|
|
2123
|
+
# Determine status
|
|
2124
|
+
if smbmap_error:
|
|
2125
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
2126
|
+
elif shares_added > 0 or findings_added > 0:
|
|
2127
|
+
status = STATUS_DONE
|
|
2128
|
+
else:
|
|
2129
|
+
status = STATUS_NO_RESULTS
|
|
2130
|
+
|
|
2014
2131
|
return {
|
|
2015
2132
|
'tool': 'smbmap',
|
|
2016
2133
|
'host': parsed['target'],
|
|
2017
2134
|
'connection_status': parsed.get('status', 'Unknown'), # SMB connection status
|
|
2018
|
-
'status':
|
|
2135
|
+
'status': status, # Job status
|
|
2019
2136
|
'shares_added': shares_added,
|
|
2020
2137
|
'files_added': files_added,
|
|
2021
2138
|
'findings_added': findings_added
|
|
@@ -2382,8 +2499,13 @@ def parse_hydra_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> D
|
|
|
2382
2499
|
)
|
|
2383
2500
|
findings_added += 1
|
|
2384
2501
|
|
|
2502
|
+
# Check for hydra errors
|
|
2503
|
+
hydra_error = detect_tool_error(log_content, 'hydra')
|
|
2504
|
+
|
|
2385
2505
|
# Determine status based on results
|
|
2386
|
-
if
|
|
2506
|
+
if hydra_error:
|
|
2507
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
2508
|
+
elif len(parsed.get('credentials', [])) > 0:
|
|
2387
2509
|
status = STATUS_DONE # Found valid credentials
|
|
2388
2510
|
elif len(parsed.get('usernames', [])) > 0:
|
|
2389
2511
|
status = STATUS_DONE # Found valid usernames (partial success is still a result)
|
|
@@ -2495,8 +2617,15 @@ def parse_nuclei_job(engagement_id: int, log_path: str, job: Dict[str, Any]) ->
|
|
|
2495
2617
|
)
|
|
2496
2618
|
findings_added += 1
|
|
2497
2619
|
|
|
2620
|
+
# Check for nuclei errors
|
|
2621
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
2622
|
+
log_content = f.read()
|
|
2623
|
+
nuclei_error = detect_tool_error(log_content, 'nuclei')
|
|
2624
|
+
|
|
2498
2625
|
# Determine status based on results
|
|
2499
|
-
if
|
|
2626
|
+
if nuclei_error:
|
|
2627
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
2628
|
+
elif parsed.get('findings_count', 0) > 0:
|
|
2500
2629
|
status = STATUS_DONE # Found vulnerabilities
|
|
2501
2630
|
else:
|
|
2502
2631
|
status = STATUS_NO_RESULTS # No vulnerabilities found
|
|
@@ -2618,6 +2747,9 @@ def parse_enum4linux_job(engagement_id: int, log_path: str, job: Dict[str, Any])
|
|
|
2618
2747
|
'ip': parsed['target']
|
|
2619
2748
|
})
|
|
2620
2749
|
|
|
2750
|
+
# Check for enum4linux errors
|
|
2751
|
+
enum4linux_error = detect_tool_error(log_content, 'enum4linux')
|
|
2752
|
+
|
|
2621
2753
|
# Determine status: done if we found any results (shares, users, or findings)
|
|
2622
2754
|
has_results = (
|
|
2623
2755
|
findings_added > 0 or
|
|
@@ -2626,9 +2758,16 @@ def parse_enum4linux_job(engagement_id: int, log_path: str, job: Dict[str, Any])
|
|
|
2626
2758
|
stats['total_shares'] > 0
|
|
2627
2759
|
)
|
|
2628
2760
|
|
|
2761
|
+
if enum4linux_error:
|
|
2762
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
2763
|
+
elif has_results:
|
|
2764
|
+
status = STATUS_DONE
|
|
2765
|
+
else:
|
|
2766
|
+
status = STATUS_NO_RESULTS
|
|
2767
|
+
|
|
2629
2768
|
return {
|
|
2630
2769
|
'tool': 'enum4linux',
|
|
2631
|
-
'status':
|
|
2770
|
+
'status': status,
|
|
2632
2771
|
'findings_added': findings_added,
|
|
2633
2772
|
'credentials_added': credentials_added,
|
|
2634
2773
|
'users_found': len(parsed['users']),
|
|
@@ -2735,13 +2874,26 @@ def parse_ffuf_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Di
|
|
|
2735
2874
|
|
|
2736
2875
|
if host_id and parsed.get('paths'):
|
|
2737
2876
|
paths_added = wpm.bulk_add_web_paths(host_id, parsed['paths'])
|
|
2738
|
-
|
|
2877
|
+
|
|
2739
2878
|
# Check for sensitive paths and create findings (same as gobuster)
|
|
2740
2879
|
created_findings = _create_findings_for_sensitive_paths(engagement_id, host_id, parsed['paths'], job)
|
|
2741
2880
|
|
|
2881
|
+
# Check for ffuf errors
|
|
2882
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
2883
|
+
log_content = f.read()
|
|
2884
|
+
ffuf_error = detect_tool_error(log_content, 'ffuf')
|
|
2885
|
+
|
|
2886
|
+
# Determine status
|
|
2887
|
+
if ffuf_error:
|
|
2888
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
2889
|
+
elif parsed.get('results_found', 0) > 0:
|
|
2890
|
+
status = STATUS_DONE
|
|
2891
|
+
else:
|
|
2892
|
+
status = STATUS_NO_RESULTS
|
|
2893
|
+
|
|
2742
2894
|
return {
|
|
2743
2895
|
'tool': 'ffuf',
|
|
2744
|
-
'status':
|
|
2896
|
+
'status': status,
|
|
2745
2897
|
'target': target,
|
|
2746
2898
|
'results_found': parsed.get('results_found', 0),
|
|
2747
2899
|
'paths_added': paths_added,
|
|
@@ -3060,8 +3212,13 @@ def parse_nikto_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> D
|
|
|
3060
3212
|
)
|
|
3061
3213
|
findings_added += 1
|
|
3062
3214
|
|
|
3215
|
+
# Check for nikto errors
|
|
3216
|
+
nikto_error = detect_tool_error(output, 'nikto')
|
|
3217
|
+
|
|
3063
3218
|
# Determine status based on results
|
|
3064
|
-
if
|
|
3219
|
+
if nikto_error:
|
|
3220
|
+
status = STATUS_ERROR # Tool failed to connect
|
|
3221
|
+
elif findings_added > 0:
|
|
3065
3222
|
status = STATUS_DONE
|
|
3066
3223
|
else:
|
|
3067
3224
|
status = STATUS_NO_RESULTS
|
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.28.0')
|
|
177
177
|
def cli():
|
|
178
178
|
"""SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
|
|
179
179
|
from souleyez.log_config import init_logging
|
|
@@ -604,6 +604,227 @@ from souleyez.commands.audit import audit
|
|
|
604
604
|
cli.add_command(audit)
|
|
605
605
|
|
|
606
606
|
|
|
607
|
+
# ============================================================================
|
|
608
|
+
# SCOPE MANAGEMENT
|
|
609
|
+
# ============================================================================
|
|
610
|
+
|
|
611
|
+
@cli.group()
|
|
612
|
+
def scope():
|
|
613
|
+
"""Engagement scope management - define and enforce target boundaries."""
|
|
614
|
+
pass
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@scope.command("add")
|
|
618
|
+
@click.argument("engagement_name")
|
|
619
|
+
@click.option("--cidr", help="Add CIDR range (e.g., 192.168.1.0/24)")
|
|
620
|
+
@click.option("--domain", help="Add domain pattern (e.g., *.example.com)")
|
|
621
|
+
@click.option("--url", help="Add URL (e.g., https://app.example.com)")
|
|
622
|
+
@click.option("--hostname", help="Add specific hostname or IP")
|
|
623
|
+
@click.option("--exclude", is_flag=True, help="Add as exclusion (deny rule)")
|
|
624
|
+
@click.option("--description", "-d", default="", help="Description for this scope entry")
|
|
625
|
+
def scope_add(engagement_name, cidr, domain, url, hostname, exclude, description):
|
|
626
|
+
"""Add a scope entry to an engagement."""
|
|
627
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
628
|
+
|
|
629
|
+
em = EngagementManager()
|
|
630
|
+
eng = em.get(engagement_name)
|
|
631
|
+
if not eng:
|
|
632
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
manager = ScopeManager()
|
|
636
|
+
|
|
637
|
+
# Determine scope type and value
|
|
638
|
+
if cidr:
|
|
639
|
+
scope_type, value = 'cidr', cidr
|
|
640
|
+
elif domain:
|
|
641
|
+
scope_type, value = 'domain', domain
|
|
642
|
+
elif url:
|
|
643
|
+
scope_type, value = 'url', url
|
|
644
|
+
elif hostname:
|
|
645
|
+
scope_type, value = 'hostname', hostname
|
|
646
|
+
else:
|
|
647
|
+
click.echo("Error: Must specify one of --cidr, --domain, --url, or --hostname", err=True)
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
scope_id = manager.add_scope(
|
|
652
|
+
engagement_id=eng['id'],
|
|
653
|
+
scope_type=scope_type,
|
|
654
|
+
value=value,
|
|
655
|
+
is_excluded=exclude,
|
|
656
|
+
description=description
|
|
657
|
+
)
|
|
658
|
+
action = "exclusion" if exclude else "scope entry"
|
|
659
|
+
click.echo(f"Added {action}: {scope_type}={value} (id={scope_id})")
|
|
660
|
+
except ValueError as e:
|
|
661
|
+
click.echo(f"Error: {e}", err=True)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@scope.command("list")
|
|
665
|
+
@click.argument("engagement_name")
|
|
666
|
+
def scope_list(engagement_name):
|
|
667
|
+
"""List scope entries for an engagement."""
|
|
668
|
+
from souleyez.security.scope_validator import ScopeManager, ScopeValidator
|
|
669
|
+
|
|
670
|
+
em = EngagementManager()
|
|
671
|
+
eng = em.get(engagement_name)
|
|
672
|
+
if not eng:
|
|
673
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
manager = ScopeManager()
|
|
677
|
+
validator = ScopeValidator(eng['id'])
|
|
678
|
+
entries = manager.list_scope(eng['id'])
|
|
679
|
+
enforcement = validator.get_enforcement_mode()
|
|
680
|
+
|
|
681
|
+
click.echo(f"\nScope for '{engagement_name}' (enforcement: {enforcement})")
|
|
682
|
+
click.echo("=" * 70)
|
|
683
|
+
|
|
684
|
+
if not entries:
|
|
685
|
+
click.echo("No scope entries defined (all targets allowed)")
|
|
686
|
+
return
|
|
687
|
+
|
|
688
|
+
click.echo(f"{'ID':<5} {'Type':<10} {'Value':<35} {'Excluded':<10}")
|
|
689
|
+
click.echo("-" * 70)
|
|
690
|
+
|
|
691
|
+
for entry in entries:
|
|
692
|
+
excluded = "EXCLUDE" if entry.get('is_excluded') else ""
|
|
693
|
+
click.echo(f"{entry['id']:<5} {entry['scope_type']:<10} {entry['value']:<35} {excluded:<10}")
|
|
694
|
+
if entry.get('description'):
|
|
695
|
+
click.echo(f" {entry['description']}")
|
|
696
|
+
|
|
697
|
+
click.echo()
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@scope.command("remove")
|
|
701
|
+
@click.argument("engagement_name")
|
|
702
|
+
@click.argument("scope_id", type=int)
|
|
703
|
+
def scope_remove(engagement_name, scope_id):
|
|
704
|
+
"""Remove a scope entry by ID."""
|
|
705
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
706
|
+
|
|
707
|
+
em = EngagementManager()
|
|
708
|
+
eng = em.get(engagement_name)
|
|
709
|
+
if not eng:
|
|
710
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
manager = ScopeManager()
|
|
714
|
+
if manager.remove_scope(scope_id):
|
|
715
|
+
click.echo(f"Removed scope entry {scope_id}")
|
|
716
|
+
else:
|
|
717
|
+
click.echo(f"Error: Failed to remove scope entry {scope_id}", err=True)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@scope.command("enforcement")
|
|
721
|
+
@click.argument("engagement_name")
|
|
722
|
+
@click.argument("mode", type=click.Choice(['off', 'warn', 'block']))
|
|
723
|
+
def scope_enforcement(engagement_name, mode):
|
|
724
|
+
"""Set enforcement mode for an engagement.
|
|
725
|
+
|
|
726
|
+
Modes:
|
|
727
|
+
off - No scope validation (default)
|
|
728
|
+
warn - Allow out-of-scope targets but log warning
|
|
729
|
+
block - Reject jobs targeting out-of-scope hosts
|
|
730
|
+
"""
|
|
731
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
732
|
+
|
|
733
|
+
em = EngagementManager()
|
|
734
|
+
eng = em.get(engagement_name)
|
|
735
|
+
if not eng:
|
|
736
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
manager = ScopeManager()
|
|
740
|
+
if manager.set_enforcement(eng['id'], mode):
|
|
741
|
+
click.echo(f"Enforcement mode set to '{mode}' for '{engagement_name}'")
|
|
742
|
+
else:
|
|
743
|
+
click.echo("Error: Failed to set enforcement mode", err=True)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@scope.command("validate")
|
|
747
|
+
@click.argument("engagement_name")
|
|
748
|
+
@click.argument("target")
|
|
749
|
+
def scope_validate(engagement_name, target):
|
|
750
|
+
"""Test if a target is in scope."""
|
|
751
|
+
from souleyez.security.scope_validator import ScopeValidator
|
|
752
|
+
|
|
753
|
+
em = EngagementManager()
|
|
754
|
+
eng = em.get(engagement_name)
|
|
755
|
+
if not eng:
|
|
756
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
validator = ScopeValidator(eng['id'])
|
|
760
|
+
result = validator.validate_target(target)
|
|
761
|
+
|
|
762
|
+
if result.is_in_scope:
|
|
763
|
+
click.echo(f"IN SCOPE: {target}")
|
|
764
|
+
if result.matched_entry:
|
|
765
|
+
click.echo(f" Matched: {result.matched_entry.get('value')}")
|
|
766
|
+
else:
|
|
767
|
+
click.echo(f"OUT OF SCOPE: {target}")
|
|
768
|
+
click.echo(f" Reason: {result.reason}")
|
|
769
|
+
|
|
770
|
+
click.echo(f" Enforcement: {validator.get_enforcement_mode()}")
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@scope.command("revalidate")
|
|
774
|
+
@click.argument("engagement_name")
|
|
775
|
+
def scope_revalidate(engagement_name):
|
|
776
|
+
"""Revalidate scope status for all hosts in an engagement."""
|
|
777
|
+
from souleyez.storage.hosts import HostManager
|
|
778
|
+
|
|
779
|
+
em = EngagementManager()
|
|
780
|
+
eng = em.get(engagement_name)
|
|
781
|
+
if not eng:
|
|
782
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
hm = HostManager()
|
|
786
|
+
result = hm.revalidate_scope_status(eng['id'])
|
|
787
|
+
|
|
788
|
+
click.echo(f"Revalidated hosts for '{engagement_name}':")
|
|
789
|
+
click.echo(f" Updated: {result['updated']}")
|
|
790
|
+
click.echo(f" In scope: {result['in_scope']}")
|
|
791
|
+
click.echo(f" Out of scope: {result['out_of_scope']}")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@scope.command("log")
|
|
795
|
+
@click.argument("engagement_name")
|
|
796
|
+
@click.option("--limit", "-n", default=50, help="Number of entries to show")
|
|
797
|
+
def scope_log(engagement_name, limit):
|
|
798
|
+
"""Show scope validation audit log."""
|
|
799
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
800
|
+
|
|
801
|
+
em = EngagementManager()
|
|
802
|
+
eng = em.get(engagement_name)
|
|
803
|
+
if not eng:
|
|
804
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
manager = ScopeManager()
|
|
808
|
+
log_entries = manager.get_validation_log(eng['id'], limit)
|
|
809
|
+
|
|
810
|
+
click.echo(f"\nScope validation log for '{engagement_name}' (last {limit})")
|
|
811
|
+
click.echo("=" * 80)
|
|
812
|
+
|
|
813
|
+
if not log_entries:
|
|
814
|
+
click.echo("No validation log entries")
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
click.echo(f"{'Time':<20} {'Target':<25} {'Result':<12} {'Action':<10}")
|
|
818
|
+
click.echo("-" * 80)
|
|
819
|
+
|
|
820
|
+
for entry in log_entries:
|
|
821
|
+
timestamp = entry.get('created_at', '')[:19] # Trim to datetime
|
|
822
|
+
target = entry.get('target', '')[:24]
|
|
823
|
+
result = entry.get('validation_result', '')
|
|
824
|
+
action = entry.get('action_taken', '')
|
|
825
|
+
click.echo(f"{timestamp:<20} {target:<25} {result:<12} {action:<10}")
|
|
826
|
+
|
|
827
|
+
|
|
607
828
|
@cli.group()
|
|
608
829
|
def jobs():
|
|
609
830
|
"""Background job management."""
|
souleyez/plugins/nuclei.py
CHANGED
|
@@ -195,8 +195,9 @@ class NucleiPlugin(PluginBase):
|
|
|
195
195
|
import os
|
|
196
196
|
from pathlib import Path
|
|
197
197
|
|
|
198
|
-
# Check common template locations
|
|
198
|
+
# Check common template locations (nuclei v3 uses ~/.local/nuclei-templates)
|
|
199
199
|
template_paths = [
|
|
200
|
+
Path.home() / ".local" / "nuclei-templates", # nuclei v3 default
|
|
200
201
|
Path.home() / "nuclei-templates",
|
|
201
202
|
Path.home() / ".nuclei-templates",
|
|
202
203
|
Path("/usr/share/nuclei-templates"),
|
souleyez/plugins/searchsploit.py
CHANGED
|
@@ -126,27 +126,30 @@ class SearchSploitPlugin(PluginBase):
|
|
|
126
126
|
|
|
127
127
|
def build_command(self, target: str, args: List[str] = None, label: str = "", log_path: str = None):
|
|
128
128
|
"""Build command for background execution with PID tracking."""
|
|
129
|
-
# Validate target
|
|
130
|
-
if not target or target.lower() == 'none':
|
|
131
|
-
raise ValueError("SearchSploit requires a search term. Usage: souleyez run searchsploit 'Apache 2.4.49'")
|
|
132
|
-
|
|
133
129
|
args = args or []
|
|
134
|
-
|
|
130
|
+
|
|
135
131
|
# Replace <target> placeholder
|
|
136
132
|
args = [arg.replace("<target>", target) for arg in args]
|
|
137
|
-
|
|
138
|
-
# searchsploit syntax: searchsploit [--json] search_term
|
|
139
|
-
cmd = ["searchsploit"]
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
|
|
134
|
+
# searchsploit syntax: searchsploit [--json] search_term
|
|
135
|
+
cmd = ["searchsploit", "--json"]
|
|
136
|
+
|
|
137
|
+
# Determine search term: use args if they contain a non-flag search term,
|
|
138
|
+
# otherwise use target (but skip if target is a URL - not a valid search term)
|
|
139
|
+
non_flag_args = [a for a in args if not a.startswith('-')]
|
|
140
|
+
|
|
141
|
+
if non_flag_args:
|
|
142
|
+
# Args contain search term(s) - use those, ignore URL target
|
|
143
|
+
cmd.extend(non_flag_args)
|
|
144
|
+
elif target and not target.startswith(('http://', 'https://')):
|
|
145
|
+
# Target is not a URL - use it as search term
|
|
146
|
+
cmd.append(target)
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError("SearchSploit requires a search term. Usage: souleyez run searchsploit 'Apache 2.4.49'")
|
|
149
|
+
|
|
150
|
+
# Add remaining flag args (excluding --json which is already added)
|
|
151
|
+
flag_args = [a for a in args if a.startswith('-') and a != '--json']
|
|
152
|
+
cmd.extend(flag_args)
|
|
150
153
|
|
|
151
154
|
return {
|
|
152
155
|
'cmd': cmd,
|