praetorian-cli 2.2.1__py3-none-any.whl → 2.2.3__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.
- praetorian_cli/handlers/add.py +25 -7
- praetorian_cli/handlers/aegis.py +107 -0
- praetorian_cli/handlers/delete.py +3 -2
- praetorian_cli/handlers/get.py +48 -2
- praetorian_cli/handlers/list.py +41 -9
- praetorian_cli/handlers/ssh_utils.py +154 -0
- praetorian_cli/handlers/test.py +7 -2
- praetorian_cli/handlers/update.py +3 -3
- praetorian_cli/main.py +1 -0
- praetorian_cli/sdk/chariot.py +71 -12
- praetorian_cli/sdk/entities/aegis.py +437 -0
- praetorian_cli/sdk/entities/assets.py +30 -12
- praetorian_cli/sdk/entities/scanners.py +13 -0
- praetorian_cli/sdk/entities/schema.py +27 -0
- praetorian_cli/sdk/entities/seeds.py +108 -56
- praetorian_cli/sdk/mcp_server.py +2 -3
- praetorian_cli/sdk/model/aegis.py +156 -0
- praetorian_cli/sdk/model/query.py +1 -1
- praetorian_cli/sdk/model/utils.py +2 -8
- praetorian_cli/sdk/test/pytest.ini +1 -0
- praetorian_cli/sdk/test/test_asset.py +2 -2
- praetorian_cli/sdk/test/test_seed.py +13 -14
- praetorian_cli/sdk/test/test_z_cli.py +22 -24
- praetorian_cli/sdk/test/ui_mocks.py +133 -0
- praetorian_cli/sdk/test/utils.py +16 -4
- praetorian_cli/ui/__init__.py +3 -0
- praetorian_cli/ui/aegis/__init__.py +5 -0
- praetorian_cli/ui/aegis/commands/__init__.py +2 -0
- praetorian_cli/ui/aegis/commands/help.py +81 -0
- praetorian_cli/ui/aegis/commands/info.py +136 -0
- praetorian_cli/ui/aegis/commands/job.py +381 -0
- praetorian_cli/ui/aegis/commands/list.py +14 -0
- praetorian_cli/ui/aegis/commands/set.py +32 -0
- praetorian_cli/ui/aegis/commands/ssh.py +87 -0
- praetorian_cli/ui/aegis/constants.py +20 -0
- praetorian_cli/ui/aegis/menu.py +395 -0
- praetorian_cli/ui/aegis/utils.py +162 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/METADATA +4 -1
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/RECORD +43 -24
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/WHEEL +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/entry_points.txt +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/licenses/LICENSE +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
import subprocess
|
|
3
|
+
from praetorian_cli.sdk.model.aegis import Agent
|
|
4
|
+
from praetorian_cli.handlers.ssh_utils import validate_agent_for_ssh
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_to_list(value, item_keys: List[str] = None) -> List:
|
|
8
|
+
keys = item_keys or ["items", "data", "capabilities", "assets"]
|
|
9
|
+
if value is None:
|
|
10
|
+
return []
|
|
11
|
+
if isinstance(value, tuple) and value:
|
|
12
|
+
value = value[0]
|
|
13
|
+
if isinstance(value, list):
|
|
14
|
+
return value
|
|
15
|
+
if isinstance(value, dict):
|
|
16
|
+
for k in keys:
|
|
17
|
+
if k in value and isinstance(value[k], list):
|
|
18
|
+
return value[k]
|
|
19
|
+
return []
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Aegis:
|
|
24
|
+
""" The methods in this class are to be accessed from sdk.aegis, where sdk
|
|
25
|
+
is an instance of Chariot. """
|
|
26
|
+
|
|
27
|
+
def __init__(self, api):
|
|
28
|
+
self.api = api
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list(self) -> tuple:
|
|
32
|
+
"""
|
|
33
|
+
List all Aegis agents.
|
|
34
|
+
|
|
35
|
+
Retrieves all Aegis agents from the account, returning them as Agent
|
|
36
|
+
objects with detailed information including system specs, network
|
|
37
|
+
interfaces, and tunnel connectivity status.
|
|
38
|
+
|
|
39
|
+
:return: A tuple containing (list of Agent objects, None for compatibility)
|
|
40
|
+
:rtype: tuple
|
|
41
|
+
|
|
42
|
+
**Example Usage:**
|
|
43
|
+
>>> # List all Aegis agents
|
|
44
|
+
>>> agents, _ = sdk.aegis.list()
|
|
45
|
+
|
|
46
|
+
>>> # Check agent properties
|
|
47
|
+
>>> for agent in agents:
|
|
48
|
+
>>> print(f"Agent: {agent.hostname}")
|
|
49
|
+
>>> print(f"OS: {agent.os}")
|
|
50
|
+
>>> print(f"Has tunnel: {agent.has_tunnel}")
|
|
51
|
+
>>> print(f"Online: {agent.is_online}")
|
|
52
|
+
|
|
53
|
+
**Agent Object Properties:**
|
|
54
|
+
- client_id: Unique identifier for the agent
|
|
55
|
+
- hostname: Agent hostname
|
|
56
|
+
- os: Operating system (e.g., 'linux', 'windows')
|
|
57
|
+
- network_interfaces: List of NetworkInterface objects
|
|
58
|
+
- has_tunnel: Boolean indicating if Cloudflare tunnel is active
|
|
59
|
+
- is_online: Boolean indicating if agent is currently online
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
agents_data = self.api.get('/aegis/agent')
|
|
63
|
+
|
|
64
|
+
# Return Agent objects
|
|
65
|
+
agents = []
|
|
66
|
+
for agent_data in agents_data:
|
|
67
|
+
agent = Agent.from_dict(agent_data)
|
|
68
|
+
agents.append(agent)
|
|
69
|
+
return agents, None
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise Exception(f"Failed to list Aegis agents: {e}")
|
|
72
|
+
|
|
73
|
+
def get_by_client_id(self, client_id: str) -> Optional[Agent]:
|
|
74
|
+
"""
|
|
75
|
+
Get a specific Aegis agent by client ID.
|
|
76
|
+
|
|
77
|
+
:param client_id: The unique client identifier for the agent
|
|
78
|
+
:type client_id: str
|
|
79
|
+
:return: Agent object if found, None if not found
|
|
80
|
+
:rtype: Agent or None
|
|
81
|
+
|
|
82
|
+
**Example Usage:**
|
|
83
|
+
>>> # Get specific agent
|
|
84
|
+
>>> agent = sdk.aegis.get_by_client_id("C.6e012b467f9faf82-OG9F0")
|
|
85
|
+
>>> if agent:
|
|
86
|
+
>>> print(f"Found agent: {agent.hostname}")
|
|
87
|
+
>>> print(f"Tunnel status: {agent.has_tunnel}")
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
agents_data, _ = self.list()
|
|
91
|
+
for agent in agents_data:
|
|
92
|
+
if agent.client_id == client_id:
|
|
93
|
+
return agent
|
|
94
|
+
return None
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise Exception(f"Failed to get agent {client_id}: {e}")
|
|
97
|
+
|
|
98
|
+
def get_capabilities(self, surface_filter: str = None, agent_os: str = None) -> List[dict]:
|
|
99
|
+
"""
|
|
100
|
+
Get Aegis capabilities with optional filtering.
|
|
101
|
+
|
|
102
|
+
Retrieves available capabilities that can be executed by Aegis agents,
|
|
103
|
+
with optional filtering by attack surface and operating system.
|
|
104
|
+
|
|
105
|
+
:param surface_filter: Filter by attack surface type (e.g., 'internal', 'external')
|
|
106
|
+
:type surface_filter: str or None
|
|
107
|
+
:param agent_os: Filter by agent operating system (e.g., 'windows', 'linux')
|
|
108
|
+
:type agent_os: str or None
|
|
109
|
+
:return: List of capability dictionaries
|
|
110
|
+
:rtype: list
|
|
111
|
+
|
|
112
|
+
**Example Usage:**
|
|
113
|
+
>>> # Get all Aegis capabilities
|
|
114
|
+
>>> caps = sdk.aegis.get_capabilities()
|
|
115
|
+
|
|
116
|
+
>>> # Get internal surface capabilities only
|
|
117
|
+
>>> internal_caps = sdk.aegis.get_capabilities(surface_filter='internal')
|
|
118
|
+
|
|
119
|
+
>>> # Get Windows capabilities for internal surface
|
|
120
|
+
>>> win_caps = sdk.aegis.get_capabilities(surface_filter='internal', agent_os='windows')
|
|
121
|
+
|
|
122
|
+
**Capability Object Properties:**
|
|
123
|
+
Each capability contains:
|
|
124
|
+
- name: Capability name (e.g., 'windows-smb-snaffler')
|
|
125
|
+
- description: Human-readable description
|
|
126
|
+
- target: Target type ('asset', 'addomain', etc.)
|
|
127
|
+
- surface: Attack surface ('internal', 'external')
|
|
128
|
+
- parameters: List of configurable parameters
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
capabilities_response = self.api.capabilities.list(executor='aegis')
|
|
132
|
+
|
|
133
|
+
# Handle different response formats
|
|
134
|
+
if isinstance(capabilities_response, tuple):
|
|
135
|
+
all_capabilities, _ = capabilities_response
|
|
136
|
+
elif isinstance(capabilities_response, list):
|
|
137
|
+
all_capabilities = capabilities_response
|
|
138
|
+
elif isinstance(capabilities_response, dict):
|
|
139
|
+
all_capabilities = capabilities_response.get('capabilities',
|
|
140
|
+
capabilities_response.get('data',
|
|
141
|
+
capabilities_response.get('items', [])))
|
|
142
|
+
else:
|
|
143
|
+
all_capabilities = []
|
|
144
|
+
|
|
145
|
+
# Ensure we have a list and all items are dicts
|
|
146
|
+
caps = normalize_to_list(all_capabilities, ["capabilities", "data", "items"]) or []
|
|
147
|
+
caps = [c for c in caps if isinstance(c, dict)]
|
|
148
|
+
|
|
149
|
+
# Apply surface filter
|
|
150
|
+
if surface_filter:
|
|
151
|
+
caps = [
|
|
152
|
+
cap for cap in caps
|
|
153
|
+
if isinstance(cap, dict) and cap.get('surface', '').lower() == surface_filter.lower()
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Apply OS filter
|
|
157
|
+
if agent_os:
|
|
158
|
+
caps = [
|
|
159
|
+
cap for cap in caps
|
|
160
|
+
if isinstance(cap, dict) and cap.get('name', '').lower().startswith(f'{agent_os.lower()}-')
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
return caps
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise Exception(f"Failed to get Aegis capabilities: {e}")
|
|
167
|
+
|
|
168
|
+
def validate_capability(self, capability_name: str) -> Optional[dict]:
|
|
169
|
+
"""
|
|
170
|
+
Validate and get capability information by name.
|
|
171
|
+
|
|
172
|
+
:param capability_name: Name of the capability to validate
|
|
173
|
+
:type capability_name: str
|
|
174
|
+
:return: Capability information if valid, None if not found
|
|
175
|
+
:rtype: dict or None
|
|
176
|
+
|
|
177
|
+
**Example Usage:**
|
|
178
|
+
>>> # Validate a capability
|
|
179
|
+
>>> cap_info = sdk.aegis.validate_capability('windows-smb-snaffler')
|
|
180
|
+
>>> if cap_info:
|
|
181
|
+
>>> print(f"Valid capability: {cap_info['name']}")
|
|
182
|
+
>>> print(f"Target type: {cap_info['target']}")
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
caps = self.get_capabilities()
|
|
186
|
+
for cap in caps:
|
|
187
|
+
if isinstance(cap, dict) and cap.get('name', '').lower() == capability_name.lower():
|
|
188
|
+
return cap
|
|
189
|
+
return None
|
|
190
|
+
except Exception:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def create_job_config(self, agent, credentials=None):
|
|
194
|
+
"""
|
|
195
|
+
Create job configuration for Aegis agent.
|
|
196
|
+
|
|
197
|
+
:param agent: Agent object containing client_id and other metadata
|
|
198
|
+
:type agent: Agent
|
|
199
|
+
:param credentials: Optional dictionary containing Username/Password for authentication
|
|
200
|
+
:type credentials: dict or None
|
|
201
|
+
:return: Job configuration dictionary
|
|
202
|
+
:rtype: dict
|
|
203
|
+
|
|
204
|
+
**Example Usage:**
|
|
205
|
+
>>> # Basic config without credentials
|
|
206
|
+
>>> config = sdk.aegis.create_job_config(agent)
|
|
207
|
+
|
|
208
|
+
>>> # Config with credentials
|
|
209
|
+
>>> creds = {"Username": "admin", "Password": "secret"}
|
|
210
|
+
>>> config = sdk.aegis.create_job_config(agent, creds)
|
|
211
|
+
"""
|
|
212
|
+
config = {
|
|
213
|
+
"aegis": "true",
|
|
214
|
+
"client_id": agent.client_id or '',
|
|
215
|
+
"manual": "true"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if credentials:
|
|
219
|
+
config.update(credentials)
|
|
220
|
+
|
|
221
|
+
return config
|
|
222
|
+
|
|
223
|
+
def get_available_ad_domains(self) -> List[str]:
|
|
224
|
+
"""
|
|
225
|
+
Get available Active Directory domains from assets.
|
|
226
|
+
|
|
227
|
+
Retrieves all AD domain assets from the account, extracting domain names
|
|
228
|
+
from both the DNS field and asset keys for comprehensive coverage.
|
|
229
|
+
|
|
230
|
+
:return: List of available AD domain names
|
|
231
|
+
:rtype: list
|
|
232
|
+
|
|
233
|
+
**Example Usage:**
|
|
234
|
+
>>> # Get all available AD domains
|
|
235
|
+
>>> domains = sdk.aegis.get_available_ad_domains()
|
|
236
|
+
>>> print(f"Found {len(domains)} domains: {domains}")
|
|
237
|
+
|
|
238
|
+
>>> # Use for job targeting
|
|
239
|
+
>>> if 'contoso.com' in domains:
|
|
240
|
+
>>> target_key = f"#addomain#contoso.com#contoso.com"
|
|
241
|
+
|
|
242
|
+
**Domain Discovery:**
|
|
243
|
+
Domains are extracted from:
|
|
244
|
+
- Asset DNS field (primary method)
|
|
245
|
+
- Asset key field format: #addomain#domain.com#domain.com (fallback)
|
|
246
|
+
"""
|
|
247
|
+
domains = []
|
|
248
|
+
try:
|
|
249
|
+
assets_resp = self.api.assets.list(asset_type='addomain')
|
|
250
|
+
|
|
251
|
+
# Handle different response formats
|
|
252
|
+
if isinstance(assets_resp, tuple):
|
|
253
|
+
assets, _ = assets_resp
|
|
254
|
+
else:
|
|
255
|
+
assets = assets_resp
|
|
256
|
+
|
|
257
|
+
if isinstance(assets, dict):
|
|
258
|
+
assets = assets.get('assets', assets.get('data', assets.get('items', [])))
|
|
259
|
+
elif isinstance(assets, list):
|
|
260
|
+
pass # assets is already a list
|
|
261
|
+
else:
|
|
262
|
+
assets = []
|
|
263
|
+
|
|
264
|
+
for asset in (assets or []):
|
|
265
|
+
if isinstance(asset, dict):
|
|
266
|
+
dns = asset.get('dns', '')
|
|
267
|
+
key = asset.get('key', '')
|
|
268
|
+
|
|
269
|
+
# Try DNS field first
|
|
270
|
+
if dns and dns not in domains:
|
|
271
|
+
domains.append(dns)
|
|
272
|
+
# If no DNS, try to extract from key format: #addomain#domain.com#domain.com
|
|
273
|
+
elif key and '#addomain#' in key:
|
|
274
|
+
parts = key.split('#')
|
|
275
|
+
if len(parts) >= 3 and parts[1] == 'addomain':
|
|
276
|
+
domain = parts[2]
|
|
277
|
+
if domain and domain not in domains:
|
|
278
|
+
domains.append(domain)
|
|
279
|
+
|
|
280
|
+
return sorted(domains)
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
raise Exception(f"Failed to get available domains: {e}")
|
|
284
|
+
|
|
285
|
+
def ssh_to_agent(self, agent: Agent, options: List[str] = None, user: str = None, display_info: bool = True) -> int:
|
|
286
|
+
"""SSH to an Aegis agent using Cloudflare tunnel."""
|
|
287
|
+
|
|
288
|
+
options = options or []
|
|
289
|
+
|
|
290
|
+
# Determine SSH username using the centralized method
|
|
291
|
+
if not user:
|
|
292
|
+
_, user = self.api.get_current_user()
|
|
293
|
+
|
|
294
|
+
is_valid, error_msg = validate_agent_for_ssh(agent)
|
|
295
|
+
if not is_valid:
|
|
296
|
+
raise Exception(error_msg)
|
|
297
|
+
|
|
298
|
+
hostname = agent.hostname or 'Unknown'
|
|
299
|
+
cf_status = agent.health_check.cloudflared_status
|
|
300
|
+
public_hostname = cf_status.hostname
|
|
301
|
+
authorized_users = cf_status.authorized_users or ''
|
|
302
|
+
tunnel_name = cf_status.tunnel_name or 'N/A'
|
|
303
|
+
|
|
304
|
+
# Check if user is authorized (if authorization is configured)
|
|
305
|
+
if authorized_users:
|
|
306
|
+
users_list = [u.strip() for u in authorized_users.split(',')]
|
|
307
|
+
if user not in users_list:
|
|
308
|
+
print(f"User '{user}' may not be authorized for tunnel. Authorized users: {', '.join(users_list)}")
|
|
309
|
+
|
|
310
|
+
ssh_command = ['ssh', '-o', 'ConnectTimeout=10', '-o', 'ServerAliveInterval=30']
|
|
311
|
+
ssh_command.extend(options)
|
|
312
|
+
ssh_command.append(f'{user}@{public_hostname}')
|
|
313
|
+
|
|
314
|
+
# Parse forwarding options for display (simple extraction from SSH flags)
|
|
315
|
+
local_forward = []
|
|
316
|
+
remote_forward = []
|
|
317
|
+
dynamic_forward = []
|
|
318
|
+
|
|
319
|
+
# Extract forwarding info for display
|
|
320
|
+
i = 0
|
|
321
|
+
while i < len(options):
|
|
322
|
+
if options[i] == '-L' and i + 1 < len(options):
|
|
323
|
+
local_forward.append(options[i + 1])
|
|
324
|
+
i += 2
|
|
325
|
+
elif options[i] == '-R' and i + 1 < len(options):
|
|
326
|
+
remote_forward.append(options[i + 1])
|
|
327
|
+
i += 2
|
|
328
|
+
elif options[i] == '-D' and i + 1 < len(options):
|
|
329
|
+
dynamic_forward.append(options[i + 1])
|
|
330
|
+
i += 2
|
|
331
|
+
else:
|
|
332
|
+
i += 1
|
|
333
|
+
|
|
334
|
+
if display_info:
|
|
335
|
+
print(f"\033[1;36m→ Connecting to {hostname}\033[0m")
|
|
336
|
+
print(f"\033[34m Gateway: {public_hostname}\033[0m")
|
|
337
|
+
print(f"\033[33m Tunnel: {tunnel_name}\033[0m")
|
|
338
|
+
print(f"\033[35m User: {user}\033[0m")
|
|
339
|
+
|
|
340
|
+
if local_forward:
|
|
341
|
+
print(f"\033[32m Local: {', '.join(local_forward)}\033[0m")
|
|
342
|
+
if remote_forward:
|
|
343
|
+
print(f"\033[31m Remote: {', '.join(remote_forward)}\033[0m")
|
|
344
|
+
if dynamic_forward:
|
|
345
|
+
socks = ', '.join([f"localhost:{p}" for p in dynamic_forward])
|
|
346
|
+
print(f"\033[35m SOCKS: {socks}\033[0m")
|
|
347
|
+
print("")
|
|
348
|
+
|
|
349
|
+
result = subprocess.run(ssh_command)
|
|
350
|
+
return result.returncode
|
|
351
|
+
|
|
352
|
+
def run_job(self, agent: Agent, capabilities: list = None, config: str = None):
|
|
353
|
+
"""
|
|
354
|
+
Run a job on an Aegis agent.
|
|
355
|
+
|
|
356
|
+
If no capabilities are provided, returns available capability dicts under
|
|
357
|
+
the 'capabilities' key. When capabilities are provided, returns a dict
|
|
358
|
+
with keys: 'success', 'job_id', 'job_key', 'status'. Errors raise.
|
|
359
|
+
"""
|
|
360
|
+
if not capabilities:
|
|
361
|
+
caps = self.get_capabilities(surface_filter='internal')
|
|
362
|
+
return {
|
|
363
|
+
'capabilities': sorted(caps, key=lambda x: x.get('name', '')),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
hostname = agent.hostname or 'unknown'
|
|
367
|
+
target_key = f"#asset#{hostname}#{hostname}"
|
|
368
|
+
|
|
369
|
+
jobs = self.api.jobs.add(target_key, list(capabilities), config)
|
|
370
|
+
if not jobs:
|
|
371
|
+
raise Exception("No job returned from API")
|
|
372
|
+
|
|
373
|
+
job = jobs[0] if isinstance(jobs, list) else jobs
|
|
374
|
+
job_key = job.get('key', '')
|
|
375
|
+
status = job.get('status', 'unknown')
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
'success': True,
|
|
379
|
+
'job_id': job_key.split('#')[-1][:12] if job_key else 'unknown',
|
|
380
|
+
'job_key': job_key,
|
|
381
|
+
'status': status,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
def format_agents_list(self, details: bool = False, filter_text: str = None):
|
|
385
|
+
"""
|
|
386
|
+
Format agents list for display with optional filtering and details.
|
|
387
|
+
|
|
388
|
+
Retrieves all Aegis agents and formats them for CLI display, with optional
|
|
389
|
+
filtering by hostname, client ID, or OS, and optional detailed information
|
|
390
|
+
including system specs and tunnel status.
|
|
391
|
+
|
|
392
|
+
:param details: Whether to show detailed agent information
|
|
393
|
+
:type details: bool
|
|
394
|
+
:param filter_text: Filter agents by hostname, client_id, or OS (case-insensitive)
|
|
395
|
+
:type filter_text: str or None
|
|
396
|
+
:return: Formatted agent list information
|
|
397
|
+
:rtype: str
|
|
398
|
+
|
|
399
|
+
**Example Usage:**
|
|
400
|
+
>>> # Simple agent list
|
|
401
|
+
>>> result = sdk.aegis.format_agents_list()
|
|
402
|
+
>>> print(result)
|
|
403
|
+
|
|
404
|
+
>>> # Detailed agent list with filtering
|
|
405
|
+
>>> result = sdk.aegis.format_agents_list(details=True, filter_text="windows")
|
|
406
|
+
>>> print(result)
|
|
407
|
+
"""
|
|
408
|
+
agents_data, _ = self.list()
|
|
409
|
+
|
|
410
|
+
if not agents_data:
|
|
411
|
+
return "No agents found."
|
|
412
|
+
|
|
413
|
+
if filter_text:
|
|
414
|
+
filter_lower = filter_text.lower()
|
|
415
|
+
agents_data = [agent for agent in agents_data
|
|
416
|
+
if filter_lower in agent.hostname.lower() or
|
|
417
|
+
filter_lower in agent.client_id.lower() or
|
|
418
|
+
filter_lower in agent.os.lower()]
|
|
419
|
+
|
|
420
|
+
if not agents_data:
|
|
421
|
+
return f"No agents found matching filter: {filter_text}"
|
|
422
|
+
|
|
423
|
+
if details:
|
|
424
|
+
detailed_lines = []
|
|
425
|
+
for i, agent in enumerate(agents_data, 1):
|
|
426
|
+
agent_details = agent.to_detailed_string()
|
|
427
|
+
# Add agent number to the first line
|
|
428
|
+
lines = agent_details.split('\n')
|
|
429
|
+
if lines:
|
|
430
|
+
lines[0] = f"[{i:2d}] {lines[0].lstrip()}"
|
|
431
|
+
detailed_lines.append('\n'.join(lines))
|
|
432
|
+
return '\n\n'.join(detailed_lines)
|
|
433
|
+
else:
|
|
434
|
+
lines = []
|
|
435
|
+
for i, agent in enumerate(agents_data, 1):
|
|
436
|
+
lines.append(f"[{i:2d}] {str(agent)}")
|
|
437
|
+
return '\n'.join(lines)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from praetorian_cli.handlers.utils import error
|
|
1
2
|
from praetorian_cli.sdk.model.globals import Asset, Kind
|
|
2
|
-
from praetorian_cli.sdk.model.query import Relationship, Node, Query, asset_of_key, RISK_NODE, ATTRIBUTE_NODE
|
|
3
|
+
from praetorian_cli.sdk.model.query import Relationship, Node, Query, Filter, KIND_TO_LABEL, asset_of_key, RISK_NODE, ATTRIBUTE_NODE
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Assets:
|
|
@@ -76,27 +77,44 @@ class Assets:
|
|
|
76
77
|
"""
|
|
77
78
|
return self.api.delete_by_key('asset', key)
|
|
78
79
|
|
|
79
|
-
def list(self,
|
|
80
|
+
def list(self, key_prefix='', asset_type='', pages=100000) -> tuple:
|
|
80
81
|
"""
|
|
81
82
|
List assets.
|
|
82
83
|
|
|
83
|
-
:param
|
|
84
|
-
:type
|
|
84
|
+
:param key_prefix: Supply this to perform prefix-filtering of the asset key. E.g., '#asset#example.com' or '#addomain#sevenkingdoms'
|
|
85
|
+
:type key_prefix: str
|
|
85
86
|
:param asset_type: The type of asset to filter by
|
|
86
87
|
:type asset_type: str
|
|
87
|
-
:param offset: The offset of the page you want to retrieve results. If this is not supplied, this function retrieves from the first page
|
|
88
|
-
:type offset: str or None
|
|
89
88
|
:param pages: The number of pages of results to retrieve. <mcp>Start with one page of results unless specifically requested.</mcp>
|
|
90
89
|
:type pages: int
|
|
91
90
|
:return: A tuple containing (list of assets, next page offset)
|
|
92
91
|
:rtype: tuple
|
|
93
92
|
"""
|
|
94
|
-
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
asset_type =
|
|
99
|
-
|
|
93
|
+
|
|
94
|
+
if asset_type in KIND_TO_LABEL:
|
|
95
|
+
asset_type = KIND_TO_LABEL[asset_type]
|
|
96
|
+
elif not asset_type:
|
|
97
|
+
asset_type = Node.Label.ASSET
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f'Invalid asset type: {asset_type}')
|
|
100
|
+
|
|
101
|
+
node = Node(
|
|
102
|
+
labels=[asset_type],
|
|
103
|
+
filters=[]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
key_filter = Filter(
|
|
107
|
+
field=Filter.Field.KEY,
|
|
108
|
+
operator=Filter.Operator.STARTS_WITH,
|
|
109
|
+
value=key_prefix
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if key_prefix:
|
|
113
|
+
node.filters.append(key_filter)
|
|
114
|
+
|
|
115
|
+
query = Query(node=node)
|
|
116
|
+
|
|
117
|
+
return self.api.search.by_query(query, pages)
|
|
100
118
|
|
|
101
119
|
def attributes(self, key):
|
|
102
120
|
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Scanners:
|
|
2
|
+
|
|
3
|
+
def __init__(self, api):
|
|
4
|
+
self.api = api
|
|
5
|
+
|
|
6
|
+
def get(self, key):
|
|
7
|
+
""" Get scanner details by exact key """
|
|
8
|
+
return self.api.search.by_exact_key(key)
|
|
9
|
+
|
|
10
|
+
def list(self, filter='', offset='', page_size=100):
|
|
11
|
+
""" List scanners with optional filtering """
|
|
12
|
+
search_term = f"#scanner#{filter}"
|
|
13
|
+
return self.api.search.by_term(search_term, None, offset, page_size)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Schema:
|
|
2
|
+
"""Access Chariot entity schemas via the SDK.
|
|
3
|
+
|
|
4
|
+
Methods in this class are accessed from `sdk.schema`, where `sdk` is an
|
|
5
|
+
instance of `Chariot`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, api):
|
|
9
|
+
self.api = api
|
|
10
|
+
|
|
11
|
+
def get(self, entity_type: str | None = None) -> dict:
|
|
12
|
+
"""Get schema information for entity types.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
entity_type: Optional specific entity type. If provided and it exists,
|
|
16
|
+
only that schema is returned. If not provided, all schemas are returned.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
dict: Schema information.
|
|
20
|
+
"""
|
|
21
|
+
result = self.api.get('schema', )
|
|
22
|
+
if entity_type:
|
|
23
|
+
if entity_type not in result:
|
|
24
|
+
return {}
|
|
25
|
+
return {entity_type: result[entity_type]}
|
|
26
|
+
return result
|
|
27
|
+
|