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/storage/hosts.py
CHANGED
|
@@ -28,9 +28,12 @@ class HostManager:
|
|
|
28
28
|
if not ip:
|
|
29
29
|
raise ValueError("Host must have an IP address")
|
|
30
30
|
|
|
31
|
+
# Determine scope status for this host
|
|
32
|
+
scope_status = self._determine_scope_status(engagement_id, ip)
|
|
33
|
+
|
|
31
34
|
# Check if host already exists
|
|
32
35
|
existing = self.db.execute_one(
|
|
33
|
-
"SELECT id FROM hosts WHERE engagement_id = ? AND ip_address = ?",
|
|
36
|
+
"SELECT id, scope_status FROM hosts WHERE engagement_id = ? AND ip_address = ?",
|
|
34
37
|
(engagement_id, ip)
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -43,6 +46,10 @@ class HostManager:
|
|
|
43
46
|
# Always update status
|
|
44
47
|
update_data['status'] = host_data.get('status', 'up')
|
|
45
48
|
|
|
49
|
+
# Update scope_status if it was unknown and we now have a determination
|
|
50
|
+
if existing.get('scope_status') == 'unknown' and scope_status != 'unknown':
|
|
51
|
+
update_data['scope_status'] = scope_status
|
|
52
|
+
|
|
46
53
|
# Only update these fields if they have values
|
|
47
54
|
if host_data.get('hostname'):
|
|
48
55
|
update_data['hostname'] = host_data['hostname']
|
|
@@ -71,11 +78,89 @@ class HostManager:
|
|
|
71
78
|
'os_name': host_data.get('os'),
|
|
72
79
|
'mac_address': host_data.get('mac_address'),
|
|
73
80
|
'os_accuracy': host_data.get('os_accuracy'),
|
|
74
|
-
'status': host_data.get('status', 'up')
|
|
81
|
+
'status': host_data.get('status', 'up'),
|
|
82
|
+
'scope_status': scope_status
|
|
75
83
|
})
|
|
76
84
|
|
|
77
85
|
return host_id
|
|
78
86
|
|
|
87
|
+
def _determine_scope_status(self, engagement_id: int, ip: str) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Determine scope status for a host based on engagement scope.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
engagement_id: Engagement ID
|
|
93
|
+
ip: IP address to check
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
'in_scope', 'out_of_scope', or 'unknown'
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
from souleyez.security.scope_validator import ScopeValidator
|
|
100
|
+
validator = ScopeValidator(engagement_id)
|
|
101
|
+
if validator.has_scope_defined():
|
|
102
|
+
result = validator.validate_ip(ip)
|
|
103
|
+
return 'in_scope' if result.is_in_scope else 'out_of_scope'
|
|
104
|
+
return 'unknown' # No scope defined
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"Failed to determine scope status for {ip}: {e}")
|
|
107
|
+
return 'unknown'
|
|
108
|
+
|
|
109
|
+
def update_scope_status(self, host_id: int, scope_status: str) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Update scope status for a host.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
host_id: Host ID
|
|
115
|
+
scope_status: 'in_scope', 'out_of_scope', or 'unknown'
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if successful
|
|
119
|
+
"""
|
|
120
|
+
valid_statuses = ['in_scope', 'out_of_scope', 'unknown']
|
|
121
|
+
if scope_status not in valid_statuses:
|
|
122
|
+
raise ValueError(f"Invalid scope_status: {scope_status}. Must be one of: {valid_statuses}")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self.db.execute(
|
|
126
|
+
"UPDATE hosts SET scope_status = ? WHERE id = ?",
|
|
127
|
+
(scope_status, host_id)
|
|
128
|
+
)
|
|
129
|
+
return True
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def revalidate_scope_status(self, engagement_id: int) -> Dict[str, int]:
|
|
134
|
+
"""
|
|
135
|
+
Revalidate scope status for all hosts in an engagement.
|
|
136
|
+
|
|
137
|
+
Call this after scope entries are added/modified to update all hosts.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
engagement_id: Engagement ID
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
{'updated': N, 'in_scope': X, 'out_of_scope': Y}
|
|
144
|
+
"""
|
|
145
|
+
hosts = self.list_hosts(engagement_id)
|
|
146
|
+
updated = 0
|
|
147
|
+
in_scope = 0
|
|
148
|
+
out_of_scope = 0
|
|
149
|
+
|
|
150
|
+
for host in hosts:
|
|
151
|
+
ip = host.get('ip') or host.get('ip_address')
|
|
152
|
+
new_status = self._determine_scope_status(engagement_id, ip)
|
|
153
|
+
if new_status != host.get('scope_status'):
|
|
154
|
+
self.update_scope_status(host['id'], new_status)
|
|
155
|
+
updated += 1
|
|
156
|
+
|
|
157
|
+
if new_status == 'in_scope':
|
|
158
|
+
in_scope += 1
|
|
159
|
+
elif new_status == 'out_of_scope':
|
|
160
|
+
out_of_scope += 1
|
|
161
|
+
|
|
162
|
+
return {'updated': updated, 'in_scope': in_scope, 'out_of_scope': out_of_scope}
|
|
163
|
+
|
|
79
164
|
def add_service(self, host_id: int, service_data: Dict[str, Any]) -> int:
|
|
80
165
|
"""
|
|
81
166
|
Add or update a service for a host.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration 026: Add Engagement Scope Validation
|
|
3
|
+
|
|
4
|
+
Tables created:
|
|
5
|
+
- engagement_scope: Structured scope definitions (CIDR, domains, URLs)
|
|
6
|
+
- scope_validation_log: Audit trail of scope validation decisions
|
|
7
|
+
|
|
8
|
+
Columns added:
|
|
9
|
+
- engagements.scope_enforcement: Enforcement mode (off, warn, block)
|
|
10
|
+
- hosts.scope_status: Host scope status (in_scope, out_of_scope, unknown)
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def upgrade(conn):
|
|
16
|
+
"""Add scope validation tables and columns."""
|
|
17
|
+
|
|
18
|
+
# Engagement scope definitions table
|
|
19
|
+
conn.execute("""
|
|
20
|
+
CREATE TABLE IF NOT EXISTS engagement_scope (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
engagement_id INTEGER NOT NULL,
|
|
23
|
+
scope_type TEXT NOT NULL,
|
|
24
|
+
value TEXT NOT NULL,
|
|
25
|
+
is_excluded BOOLEAN DEFAULT 0,
|
|
26
|
+
description TEXT,
|
|
27
|
+
added_by TEXT,
|
|
28
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
FOREIGN KEY (engagement_id) REFERENCES engagements(id) ON DELETE CASCADE,
|
|
30
|
+
UNIQUE(engagement_id, scope_type, value)
|
|
31
|
+
)
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
# Scope validation audit log
|
|
35
|
+
conn.execute("""
|
|
36
|
+
CREATE TABLE IF NOT EXISTS scope_validation_log (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
engagement_id INTEGER NOT NULL,
|
|
39
|
+
job_id INTEGER,
|
|
40
|
+
target TEXT NOT NULL,
|
|
41
|
+
validation_result TEXT NOT NULL,
|
|
42
|
+
action_taken TEXT NOT NULL,
|
|
43
|
+
matched_scope_id INTEGER,
|
|
44
|
+
user_id TEXT,
|
|
45
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
FOREIGN KEY (engagement_id) REFERENCES engagements(id) ON DELETE CASCADE
|
|
47
|
+
)
|
|
48
|
+
""")
|
|
49
|
+
|
|
50
|
+
# Add scope_enforcement column to engagements
|
|
51
|
+
try:
|
|
52
|
+
conn.execute("ALTER TABLE engagements ADD COLUMN scope_enforcement TEXT DEFAULT 'off'")
|
|
53
|
+
except Exception:
|
|
54
|
+
pass # Column may already exist
|
|
55
|
+
|
|
56
|
+
# Add scope_status column to hosts
|
|
57
|
+
try:
|
|
58
|
+
conn.execute("ALTER TABLE hosts ADD COLUMN scope_status TEXT DEFAULT 'unknown'")
|
|
59
|
+
except Exception:
|
|
60
|
+
pass # Column may already exist
|
|
61
|
+
|
|
62
|
+
# Indexes for performance
|
|
63
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_scope_engagement ON engagement_scope(engagement_id)")
|
|
64
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_scope_type ON engagement_scope(scope_type)")
|
|
65
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_scope_log_engagement ON scope_validation_log(engagement_id)")
|
|
66
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_scope_log_result ON scope_validation_log(validation_result)")
|
|
67
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_scope_log_timestamp ON scope_validation_log(created_at DESC)")
|
|
68
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_hosts_scope_status ON hosts(scope_status)")
|
|
69
|
+
|
|
70
|
+
conn.commit()
|
|
71
|
+
|
|
72
|
+
if not os.environ.get('SOULEYEZ_MIGRATION_SILENT'):
|
|
73
|
+
print("Migration 026: Engagement scope validation tables created")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def downgrade(conn):
|
|
77
|
+
"""Remove scope validation tables."""
|
|
78
|
+
conn.execute("DROP TABLE IF EXISTS scope_validation_log")
|
|
79
|
+
conn.execute("DROP TABLE IF EXISTS engagement_scope")
|
|
80
|
+
conn.execute("DROP INDEX IF EXISTS idx_scope_engagement")
|
|
81
|
+
conn.execute("DROP INDEX IF EXISTS idx_scope_type")
|
|
82
|
+
conn.execute("DROP INDEX IF EXISTS idx_scope_log_engagement")
|
|
83
|
+
conn.execute("DROP INDEX IF EXISTS idx_scope_log_result")
|
|
84
|
+
conn.execute("DROP INDEX IF EXISTS idx_scope_log_timestamp")
|
|
85
|
+
conn.execute("DROP INDEX IF EXISTS idx_hosts_scope_status")
|
|
86
|
+
# Note: Cannot easily drop columns in SQLite, they remain but unused
|
|
87
|
+
print("Migration 026: Engagement scope validation tables dropped")
|
souleyez/ui/interactive.py
CHANGED
|
@@ -7172,6 +7172,275 @@ def view_results_menu():
|
|
|
7172
7172
|
return
|
|
7173
7173
|
|
|
7174
7174
|
|
|
7175
|
+
def _scope_management_menu(engagement_id: int, engagement_name: str):
|
|
7176
|
+
"""Interactive scope management menu for an engagement."""
|
|
7177
|
+
from souleyez.security.scope_validator import ScopeManager, ScopeValidator
|
|
7178
|
+
from souleyez.storage.hosts import HostManager
|
|
7179
|
+
from rich.console import Console
|
|
7180
|
+
from rich.table import Table
|
|
7181
|
+
|
|
7182
|
+
manager = ScopeManager()
|
|
7183
|
+
validator = ScopeValidator(engagement_id)
|
|
7184
|
+
|
|
7185
|
+
while True:
|
|
7186
|
+
DesignSystem.clear_screen()
|
|
7187
|
+
render_standard_header(f"SCOPE MANAGEMENT - {engagement_name}")
|
|
7188
|
+
|
|
7189
|
+
# Get current scope info
|
|
7190
|
+
entries = manager.list_scope(engagement_id)
|
|
7191
|
+
enforcement = validator.get_enforcement_mode()
|
|
7192
|
+
|
|
7193
|
+
# Show enforcement status
|
|
7194
|
+
if enforcement == 'block':
|
|
7195
|
+
enf_color = 'red'
|
|
7196
|
+
enf_desc = 'Block out-of-scope targets'
|
|
7197
|
+
elif enforcement == 'warn':
|
|
7198
|
+
enf_color = 'yellow'
|
|
7199
|
+
enf_desc = 'Warn but allow'
|
|
7200
|
+
else:
|
|
7201
|
+
enf_color = 'bright_black'
|
|
7202
|
+
enf_desc = 'No validation'
|
|
7203
|
+
|
|
7204
|
+
click.echo()
|
|
7205
|
+
click.echo(f" Enforcement: {click.style(enforcement.upper(), fg=enf_color, bold=True)} - {enf_desc}")
|
|
7206
|
+
click.echo()
|
|
7207
|
+
|
|
7208
|
+
# Display scope entries
|
|
7209
|
+
if not entries:
|
|
7210
|
+
click.echo(click.style(" No scope entries defined - all targets allowed", fg='yellow'))
|
|
7211
|
+
click.echo()
|
|
7212
|
+
else:
|
|
7213
|
+
console = Console()
|
|
7214
|
+
table = Table(show_header=True, header_style="bold", box=DesignSystem.TABLE_BOX, padding=(0, 1))
|
|
7215
|
+
|
|
7216
|
+
table.add_column("ID", width=4, justify="right")
|
|
7217
|
+
table.add_column("Type", width=10)
|
|
7218
|
+
table.add_column("Value", min_width=30)
|
|
7219
|
+
table.add_column("Status", width=10)
|
|
7220
|
+
table.add_column("Description", max_width=25)
|
|
7221
|
+
|
|
7222
|
+
for entry in entries:
|
|
7223
|
+
status = "[red]EXCLUDE[/red]" if entry.get('is_excluded') else "[green]INCLUDE[/green]"
|
|
7224
|
+
table.add_row(
|
|
7225
|
+
str(entry['id']),
|
|
7226
|
+
entry['scope_type'],
|
|
7227
|
+
entry['value'],
|
|
7228
|
+
status,
|
|
7229
|
+
entry.get('description', '') or ''
|
|
7230
|
+
)
|
|
7231
|
+
|
|
7232
|
+
console.print(table)
|
|
7233
|
+
click.echo()
|
|
7234
|
+
|
|
7235
|
+
click.echo("─" * get_terminal_width())
|
|
7236
|
+
click.echo()
|
|
7237
|
+
click.echo(" [+] Add scope entry")
|
|
7238
|
+
click.echo(" [-] Remove scope entry")
|
|
7239
|
+
click.echo(" [e] Change enforcement mode")
|
|
7240
|
+
click.echo(" [t] Test target validation")
|
|
7241
|
+
click.echo(" [r] Revalidate all hosts")
|
|
7242
|
+
click.echo(" [l] View validation log")
|
|
7243
|
+
click.echo(" [q] Back")
|
|
7244
|
+
click.echo()
|
|
7245
|
+
|
|
7246
|
+
try:
|
|
7247
|
+
choice = click.prompt(" Select option", type=str, default='q', show_default=False).strip().lower()
|
|
7248
|
+
|
|
7249
|
+
if choice == 'q':
|
|
7250
|
+
return
|
|
7251
|
+
|
|
7252
|
+
elif choice == '+':
|
|
7253
|
+
# Add scope entry
|
|
7254
|
+
click.echo()
|
|
7255
|
+
click.echo(click.style(" Add Scope Entry", fg='cyan', bold=True))
|
|
7256
|
+
click.echo()
|
|
7257
|
+
click.echo(" Type:")
|
|
7258
|
+
click.echo(" [1] CIDR range (e.g., 192.168.1.0/24)")
|
|
7259
|
+
click.echo(" [2] Domain pattern (e.g., *.example.com)")
|
|
7260
|
+
click.echo(" [3] URL (e.g., https://app.example.com)")
|
|
7261
|
+
click.echo(" [4] Hostname/IP (exact match)")
|
|
7262
|
+
click.echo(" [c] Cancel")
|
|
7263
|
+
click.echo()
|
|
7264
|
+
|
|
7265
|
+
type_choice = click.prompt(" Select type", type=str, default='c').strip().lower()
|
|
7266
|
+
|
|
7267
|
+
if type_choice == 'c':
|
|
7268
|
+
continue
|
|
7269
|
+
|
|
7270
|
+
type_map = {'1': 'cidr', '2': 'domain', '3': 'url', '4': 'hostname'}
|
|
7271
|
+
scope_type = type_map.get(type_choice)
|
|
7272
|
+
|
|
7273
|
+
if not scope_type:
|
|
7274
|
+
click.echo(click.style("\n Invalid choice!", fg='red'))
|
|
7275
|
+
click.pause()
|
|
7276
|
+
continue
|
|
7277
|
+
|
|
7278
|
+
# Get value
|
|
7279
|
+
if scope_type == 'cidr':
|
|
7280
|
+
value_hint = "192.168.1.0/24"
|
|
7281
|
+
elif scope_type == 'domain':
|
|
7282
|
+
value_hint = "*.example.com or example.com"
|
|
7283
|
+
elif scope_type == 'url':
|
|
7284
|
+
value_hint = "https://app.example.com"
|
|
7285
|
+
else:
|
|
7286
|
+
value_hint = "10.0.0.1 or server.local"
|
|
7287
|
+
|
|
7288
|
+
value = click.prompt(f"\n Enter {scope_type} ({value_hint})", type=str).strip()
|
|
7289
|
+
if not value:
|
|
7290
|
+
continue
|
|
7291
|
+
|
|
7292
|
+
# Ask if exclusion
|
|
7293
|
+
is_excluded = click.confirm(" Is this an EXCLUSION (deny rule)?", default=False)
|
|
7294
|
+
|
|
7295
|
+
# Optional description
|
|
7296
|
+
description = click.prompt(" Description (optional)", type=str, default="").strip()
|
|
7297
|
+
|
|
7298
|
+
try:
|
|
7299
|
+
scope_id = manager.add_scope(
|
|
7300
|
+
engagement_id=engagement_id,
|
|
7301
|
+
scope_type=scope_type,
|
|
7302
|
+
value=value,
|
|
7303
|
+
is_excluded=is_excluded,
|
|
7304
|
+
description=description or None
|
|
7305
|
+
)
|
|
7306
|
+
entry_type = "exclusion" if is_excluded else "scope entry"
|
|
7307
|
+
click.echo(click.style(f"\n ✓ Added {entry_type}: {scope_type}={value} (ID: {scope_id})", fg='green'))
|
|
7308
|
+
except ValueError as e:
|
|
7309
|
+
click.echo(click.style(f"\n ✗ Invalid value: {e}", fg='red'))
|
|
7310
|
+
except Exception as e:
|
|
7311
|
+
if "UNIQUE constraint" in str(e):
|
|
7312
|
+
click.echo(click.style(f"\n ✗ This scope entry already exists!", fg='red'))
|
|
7313
|
+
else:
|
|
7314
|
+
click.echo(click.style(f"\n ✗ Error: {e}", fg='red'))
|
|
7315
|
+
click.pause()
|
|
7316
|
+
|
|
7317
|
+
elif choice == '-':
|
|
7318
|
+
# Remove scope entry
|
|
7319
|
+
if not entries:
|
|
7320
|
+
click.echo(click.style("\n No scope entries to remove!", fg='yellow'))
|
|
7321
|
+
click.pause()
|
|
7322
|
+
continue
|
|
7323
|
+
|
|
7324
|
+
try:
|
|
7325
|
+
scope_id = click.prompt("\n Enter scope ID to remove", type=int)
|
|
7326
|
+
entry = next((e for e in entries if e['id'] == scope_id), None)
|
|
7327
|
+
|
|
7328
|
+
if entry:
|
|
7329
|
+
if click.confirm(f" Remove '{entry['scope_type']}={entry['value']}'?", default=False):
|
|
7330
|
+
if manager.remove_scope(scope_id):
|
|
7331
|
+
click.echo(click.style(f"\n ✓ Removed scope entry {scope_id}", fg='green'))
|
|
7332
|
+
else:
|
|
7333
|
+
click.echo(click.style(f"\n ✗ Failed to remove scope entry!", fg='red'))
|
|
7334
|
+
else:
|
|
7335
|
+
click.echo(click.style(f"\n ✗ Scope ID {scope_id} not found!", fg='red'))
|
|
7336
|
+
except ValueError:
|
|
7337
|
+
click.echo(click.style("\n ✗ Invalid ID!", fg='red'))
|
|
7338
|
+
click.pause()
|
|
7339
|
+
|
|
7340
|
+
elif choice == 'e':
|
|
7341
|
+
# Change enforcement mode
|
|
7342
|
+
click.echo()
|
|
7343
|
+
click.echo(click.style(" Enforcement Mode", fg='cyan', bold=True))
|
|
7344
|
+
click.echo()
|
|
7345
|
+
click.echo(f" Current: {click.style(enforcement.upper(), fg=enf_color, bold=True)}")
|
|
7346
|
+
click.echo()
|
|
7347
|
+
click.echo(" [1] OFF - No scope validation")
|
|
7348
|
+
click.echo(" [2] WARN - Allow but log warning")
|
|
7349
|
+
click.echo(" [3] BLOCK - Reject out-of-scope targets")
|
|
7350
|
+
click.echo(" [c] Cancel")
|
|
7351
|
+
click.echo()
|
|
7352
|
+
|
|
7353
|
+
mode_choice = click.prompt(" Select mode", type=str, default='c').strip().lower()
|
|
7354
|
+
|
|
7355
|
+
mode_map = {'1': 'off', '2': 'warn', '3': 'block'}
|
|
7356
|
+
new_mode = mode_map.get(mode_choice)
|
|
7357
|
+
|
|
7358
|
+
if new_mode:
|
|
7359
|
+
if manager.set_enforcement(engagement_id, new_mode):
|
|
7360
|
+
click.echo(click.style(f"\n ✓ Enforcement mode set to {new_mode.upper()}", fg='green'))
|
|
7361
|
+
# Refresh validator cache
|
|
7362
|
+
validator = ScopeValidator(engagement_id)
|
|
7363
|
+
else:
|
|
7364
|
+
click.echo(click.style("\n ✗ Failed to set enforcement mode!", fg='red'))
|
|
7365
|
+
click.pause()
|
|
7366
|
+
|
|
7367
|
+
elif choice == 't':
|
|
7368
|
+
# Test target validation
|
|
7369
|
+
click.echo()
|
|
7370
|
+
target = click.prompt(" Enter target to test", type=str).strip()
|
|
7371
|
+
if target:
|
|
7372
|
+
result = validator.validate_target(target)
|
|
7373
|
+
click.echo()
|
|
7374
|
+
if result.is_in_scope:
|
|
7375
|
+
click.echo(click.style(f" ✓ IN SCOPE: {target}", fg='green', bold=True))
|
|
7376
|
+
if result.matched_entry:
|
|
7377
|
+
click.echo(f" Matched: {result.matched_entry.get('value')}")
|
|
7378
|
+
else:
|
|
7379
|
+
click.echo(click.style(f" ✗ OUT OF SCOPE: {target}", fg='red', bold=True))
|
|
7380
|
+
click.echo(f" Reason: {result.reason}")
|
|
7381
|
+
click.echo(f" Enforcement: {enforcement}")
|
|
7382
|
+
click.pause()
|
|
7383
|
+
|
|
7384
|
+
elif choice == 'r':
|
|
7385
|
+
# Revalidate all hosts
|
|
7386
|
+
click.echo()
|
|
7387
|
+
if click.confirm(" Revalidate scope status for all hosts in this engagement?", default=True):
|
|
7388
|
+
hm = HostManager()
|
|
7389
|
+
result = hm.revalidate_scope_status(engagement_id)
|
|
7390
|
+
click.echo()
|
|
7391
|
+
click.echo(click.style(" Revalidation complete:", fg='green'))
|
|
7392
|
+
click.echo(f" Updated: {result['updated']}")
|
|
7393
|
+
click.echo(f" In scope: {result['in_scope']}")
|
|
7394
|
+
click.echo(f" Out of scope: {result['out_of_scope']}")
|
|
7395
|
+
click.pause()
|
|
7396
|
+
|
|
7397
|
+
elif choice == 'l':
|
|
7398
|
+
# View validation log
|
|
7399
|
+
log_entries = manager.get_validation_log(engagement_id, limit=30)
|
|
7400
|
+
click.echo()
|
|
7401
|
+
click.echo(click.style(" Recent Scope Validation Log (last 30)", fg='cyan', bold=True))
|
|
7402
|
+
click.echo()
|
|
7403
|
+
|
|
7404
|
+
if not log_entries:
|
|
7405
|
+
click.echo(" No validation log entries yet.")
|
|
7406
|
+
else:
|
|
7407
|
+
console = Console()
|
|
7408
|
+
table = Table(show_header=True, header_style="bold", box=DesignSystem.TABLE_BOX, padding=(0, 1))
|
|
7409
|
+
|
|
7410
|
+
table.add_column("Time", width=19)
|
|
7411
|
+
table.add_column("Target", min_width=20)
|
|
7412
|
+
table.add_column("Result", width=12)
|
|
7413
|
+
table.add_column("Action", width=10)
|
|
7414
|
+
table.add_column("Job", width=6)
|
|
7415
|
+
|
|
7416
|
+
for entry in log_entries:
|
|
7417
|
+
timestamp = entry.get('created_at', '')[:19]
|
|
7418
|
+
target = entry.get('target', '')
|
|
7419
|
+
result_val = entry.get('validation_result', '')
|
|
7420
|
+
action = entry.get('action_taken', '')
|
|
7421
|
+
job_id = str(entry.get('job_id', '-') or '-')
|
|
7422
|
+
|
|
7423
|
+
# Color code results
|
|
7424
|
+
if result_val == 'in_scope':
|
|
7425
|
+
result_display = "[green]in_scope[/green]"
|
|
7426
|
+
elif result_val == 'out_of_scope':
|
|
7427
|
+
result_display = "[red]out_of_scope[/red]"
|
|
7428
|
+
else:
|
|
7429
|
+
result_display = result_val
|
|
7430
|
+
|
|
7431
|
+
table.add_row(timestamp, target, result_display, action, job_id)
|
|
7432
|
+
|
|
7433
|
+
console.print(table)
|
|
7434
|
+
click.pause()
|
|
7435
|
+
|
|
7436
|
+
else:
|
|
7437
|
+
click.echo(click.style("\n Invalid choice!", fg='red'))
|
|
7438
|
+
click.pause()
|
|
7439
|
+
|
|
7440
|
+
except (KeyboardInterrupt, EOFError):
|
|
7441
|
+
return
|
|
7442
|
+
|
|
7443
|
+
|
|
7175
7444
|
def _engagements_bulk_action_menu(selected_ids: set, em, current_id: int):
|
|
7176
7445
|
"""Show bulk action menu for selected engagements."""
|
|
7177
7446
|
click.echo()
|
|
@@ -7426,6 +7695,7 @@ def manage_engagements_menu():
|
|
|
7426
7695
|
click.echo(" [a] All - Toggle pagination")
|
|
7427
7696
|
click.echo(" [+] Create - Create new engagement")
|
|
7428
7697
|
click.echo(" [-] Delete - Delete engagement(s)")
|
|
7698
|
+
click.echo(" [s] Scope - Manage target scope")
|
|
7429
7699
|
click.echo(" [d] Deliverables - Track progress")
|
|
7430
7700
|
click.echo(" [q] Back")
|
|
7431
7701
|
|
|
@@ -7460,6 +7730,15 @@ def manage_engagements_menu():
|
|
|
7460
7730
|
current_page = 0
|
|
7461
7731
|
continue
|
|
7462
7732
|
|
|
7733
|
+
elif choice_input == 's':
|
|
7734
|
+
# Scope Management
|
|
7735
|
+
if not current_ws:
|
|
7736
|
+
click.echo(click.style("\n ⚠️ Please select an engagement first!", fg='yellow'))
|
|
7737
|
+
click.pause()
|
|
7738
|
+
continue
|
|
7739
|
+
_scope_management_menu(current_ws['id'], current_ws['name'])
|
|
7740
|
+
continue
|
|
7741
|
+
|
|
7463
7742
|
elif choice_input == 'd':
|
|
7464
7743
|
# Deliverables Tracker
|
|
7465
7744
|
if not current_ws:
|
|
@@ -30962,31 +31241,36 @@ def view_documentation_menu():
|
|
|
30962
31241
|
'name': 'Keyboard Shortcuts',
|
|
30963
31242
|
'file': None, # Special handler
|
|
30964
31243
|
'description': 'Command Center and Dashboard shortcuts'
|
|
31244
|
+
},
|
|
31245
|
+
'8': {
|
|
31246
|
+
'name': 'Scope Management Guide',
|
|
31247
|
+
'file': 'docs/user-guide/scope-management.md',
|
|
31248
|
+
'description': 'Target validation and boundary enforcement'
|
|
30965
31249
|
}
|
|
30966
31250
|
}
|
|
30967
31251
|
|
|
30968
31252
|
# Add PRO-only documentation
|
|
30969
31253
|
if is_pro:
|
|
30970
31254
|
docs.update({
|
|
30971
|
-
'
|
|
31255
|
+
'9': {
|
|
30972
31256
|
'name': 'MSF Integration Guide',
|
|
30973
31257
|
'file': None, # Special handler
|
|
30974
31258
|
'description': 'Metasploit Framework workflows',
|
|
30975
31259
|
'pro': True
|
|
30976
31260
|
},
|
|
30977
|
-
'
|
|
31261
|
+
'10': {
|
|
30978
31262
|
'name': 'RBAC & User Management',
|
|
30979
31263
|
'file': 'docs/user-guide/rbac.md',
|
|
30980
31264
|
'description': 'Roles, permissions, and audit logging',
|
|
30981
31265
|
'pro': True
|
|
30982
31266
|
},
|
|
30983
|
-
'
|
|
31267
|
+
'11': {
|
|
30984
31268
|
'name': 'SIEM Integration Guide',
|
|
30985
31269
|
'file': 'docs/user-guide/siem-integration.md',
|
|
30986
31270
|
'description': 'Splunk, Wazuh, Elastic, Sentinel setup',
|
|
30987
31271
|
'pro': True
|
|
30988
31272
|
},
|
|
30989
|
-
'
|
|
31273
|
+
'12': {
|
|
30990
31274
|
'name': 'AI Integration Guide',
|
|
30991
31275
|
'file': 'docs/user-guide/ai-integration.md',
|
|
30992
31276
|
'description': 'AI providers and autonomous execution',
|
|
@@ -31055,7 +31339,7 @@ def view_documentation_menu():
|
|
|
31055
31339
|
continue
|
|
31056
31340
|
|
|
31057
31341
|
# Special handler for MSF Integration Guide
|
|
31058
|
-
if choice == '
|
|
31342
|
+
if choice == '9':
|
|
31059
31343
|
_show_msf_help()
|
|
31060
31344
|
continue
|
|
31061
31345
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: souleyez
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.28.0
|
|
4
4
|
Summary: AI-Powered Penetration Testing Platform with 40+ integrated tools
|
|
5
5
|
Author-email: CyberSoul Security <contact@cybersoulsecurity.com>
|
|
6
6
|
Maintainer-email: CyberSoul Security <contact@cybersoulsecurity.com>
|
|
@@ -51,6 +51,12 @@ Dynamic: license-file
|
|
|
51
51
|
|
|
52
52
|
# SoulEyez Beta Program
|
|
53
53
|
|
|
54
|
+
[](https://github.com/cyber-soul-security/souleyez/actions/workflows/python-ci.yml)
|
|
55
|
+
[](https://codecov.io/gh/cyber-soul-security/souleyez)
|
|
56
|
+
[](https://www.python.org/downloads/)
|
|
57
|
+
[](https://github.com/psf/black)
|
|
58
|
+
[](https://github.com/PyCQA/bandit)
|
|
59
|
+
|
|
54
60
|
Welcome to the SoulEyez beta! Thank you for helping us test and improve this penetration testing management platform.
|
|
55
61
|
|
|
56
62
|
---
|
|
@@ -72,7 +78,7 @@ Welcome to the SoulEyez beta! Thank you for helping us test and improve this pen
|
|
|
72
78
|
|
|
73
79
|
> ⚠️ **Important**: Only use SoulEyez on systems you have explicit authorization to test.
|
|
74
80
|
|
|
75
|
-
## Version: 2.
|
|
81
|
+
## Version: 2.28.0
|
|
76
82
|
|
|
77
83
|
### What's Included
|
|
78
84
|
|
|
@@ -310,4 +316,4 @@ Happy hacking! 🛡️
|
|
|
310
316
|
|
|
311
317
|
---
|
|
312
318
|
|
|
313
|
-
**Version**: 2.
|
|
319
|
+
**Version**: 2.28.0 | **Release Date**: January 2026 | **Maintainer**: CyberSoul Security
|