praetorian-cli 2.4.0__tar.gz → 2.4.2__tar.gz
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-2.4.0/praetorian_cli.egg-info → praetorian_cli-2.4.2}/PKG-INFO +1 -1
- praetorian_cli-2.4.2/praetorian_cli/handlers/access.py +209 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/configure.py +1 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/engagement.py +7 -4
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/export.py +4 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/list.py +21 -2
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/run.py +96 -12
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/main.py +1 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/runners/local.py +4 -1
- praetorian_cli-2.4.2/praetorian_cli/sdk/entities/account_discovery.py +443 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/assets.py +25 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/attributes.py +15 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/credentials.py +20 -3
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/jobs.py +171 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/reports.py +5 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/risks.py +24 -0
- praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_access_aws.py +198 -0
- praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_bulk.py +315 -0
- praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_credential_process.py +63 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_job.py +44 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_report.py +3 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_z_cli.py +2 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/ui_mocks.py +5 -0
- praetorian_cli-2.4.2/praetorian_cli/ui/aegis/account_selector.py +250 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/job.py +2 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/job_helpers.py +10 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/list.py +1 -3
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/schedule.py +262 -166
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/set.py +27 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/menu.py +120 -19
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/__init__.py +1 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/accounts.py +6 -2
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/context.py +70 -2
- praetorian_cli-2.4.2/praetorian_cli/ui/console/commands/data.py +162 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/marcus.py +109 -14
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/search.py +36 -15
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/console.py +41 -4
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/context.py +1 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/renderer.py +151 -6
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2/praetorian_cli.egg-info}/PKG-INFO +1 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/SOURCES.txt +7 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/setup.cfg +1 -1
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/LICENSE +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/MANIFEST.in +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/README.md +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/add.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/aegis.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/agent.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/critfinder.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/find.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/get.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/report.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/ssh_utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/update.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/runners/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/chariot.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/accounts.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/ad.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/aegis.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/scanners.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/schedules.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/schema.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/webhook.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/webpage.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/guard.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/mcp_server.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/aegis.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_asset.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_conversation.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_mcp.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_risk.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_search.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_webpage.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/help.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/info.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/constants.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/theme.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/utils.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/reporting.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/tools.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/conversation/__init__.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/requires.txt +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/pyproject.toml +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from praetorian_cli.handlers.chariot import chariot
|
|
6
|
+
from praetorian_cli.handlers.cli_decorators import cli_handler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_prefix(email):
|
|
10
|
+
"""Extract the profile prefix from a Guard account email.
|
|
11
|
+
|
|
12
|
+
Takes the portion after '+' and before '@'.
|
|
13
|
+
If no '+' exists, uses the full local part (before '@').
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
chariot+proceptbiorobotics@praetorian.com -> proceptbiorobotics
|
|
17
|
+
chariot+grant_street_group-ztw@praetorian.com -> grant_street_group-ztw
|
|
18
|
+
user@example.com -> user
|
|
19
|
+
"""
|
|
20
|
+
local_part = email.split('@')[0]
|
|
21
|
+
if '+' in local_part:
|
|
22
|
+
return local_part.split('+', 1)[1]
|
|
23
|
+
return local_part
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_account_id(credential_key):
|
|
27
|
+
"""Extract the AWS account ID from a credential key.
|
|
28
|
+
|
|
29
|
+
Keys look like: #account#chariot+client@praetorian.com#amazon#325281727610
|
|
30
|
+
The account ID is the last segment.
|
|
31
|
+
"""
|
|
32
|
+
return credential_key.rstrip('#').split('#')[-1]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_aws_config_profiles(account_email, prefix, credential_id, root_account_id, sub_accounts, category='env-integration'):
|
|
36
|
+
"""Build a list of (profile_name, profile_dict) tuples for AWS config.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
account_email: The Guard account email (used in credential_process command)
|
|
40
|
+
prefix: The profile name prefix
|
|
41
|
+
credential_id: The credential UUID
|
|
42
|
+
root_account_id: The root AWS account ID
|
|
43
|
+
sub_accounts: List of dicts with 'Id' key from Organizations list_accounts,
|
|
44
|
+
or empty list if not an org.
|
|
45
|
+
category: The credential category (e.g., 'env-integration', 'cloud')
|
|
46
|
+
Returns:
|
|
47
|
+
List of (profile_name, dict) where dict has credential_process, region, output keys.
|
|
48
|
+
"""
|
|
49
|
+
profiles = []
|
|
50
|
+
|
|
51
|
+
# Collect all account IDs (root + sub-accounts), deduplicated
|
|
52
|
+
all_account_ids = [root_account_id]
|
|
53
|
+
for acct in sub_accounts:
|
|
54
|
+
if acct['Id'] != root_account_id:
|
|
55
|
+
all_account_ids.append(acct['Id'])
|
|
56
|
+
|
|
57
|
+
for account_id in all_account_ids:
|
|
58
|
+
profile_name = f'{prefix}-{account_id}'
|
|
59
|
+
credential_process = (
|
|
60
|
+
f'guard --account {account_email} get credential {credential_id} '
|
|
61
|
+
f'--category {category} --type aws --format credential-process '
|
|
62
|
+
f'--parameters accountId {account_id}'
|
|
63
|
+
)
|
|
64
|
+
profiles.append((profile_name, {
|
|
65
|
+
'credential_process': credential_process,
|
|
66
|
+
'region': 'us-east-1',
|
|
67
|
+
'output': 'json',
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
return profiles
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_aws_config(profiles, config_path=None):
|
|
74
|
+
"""Write AWS config profiles, preserving existing unrelated profiles.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
profiles: List of (profile_name, profile_dict) tuples.
|
|
78
|
+
config_path: Path to AWS config file. Defaults to ~/.aws/config.
|
|
79
|
+
"""
|
|
80
|
+
from configparser import ConfigParser
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
if config_path is None:
|
|
84
|
+
config_path = os.path.join(Path.home(), '.aws', 'config')
|
|
85
|
+
|
|
86
|
+
config = ConfigParser()
|
|
87
|
+
if os.path.exists(config_path):
|
|
88
|
+
config.read(config_path)
|
|
89
|
+
|
|
90
|
+
for profile_name, profile_data in profiles:
|
|
91
|
+
section = f'profile {profile_name}'
|
|
92
|
+
if not config.has_section(section):
|
|
93
|
+
config.add_section(section)
|
|
94
|
+
for key, value in profile_data.items():
|
|
95
|
+
config.set(section, key, value)
|
|
96
|
+
|
|
97
|
+
parent_dir = os.path.dirname(config_path)
|
|
98
|
+
if parent_dir:
|
|
99
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
100
|
+
with open(config_path, 'w') as f:
|
|
101
|
+
config.write(f)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@chariot.group()
|
|
105
|
+
def access():
|
|
106
|
+
"""Configure credential access for cloud providers."""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@access.command()
|
|
111
|
+
@cli_handler
|
|
112
|
+
@click.option('--account', default=None, help='Guard account email to discover AWS credentials for (also inherited from guard --account)')
|
|
113
|
+
@click.option('--prefix', default=None, help='Override the profile name prefix (default: derived from email)')
|
|
114
|
+
def aws(sdk, account, prefix):
|
|
115
|
+
"""Generate AWS CLI config profiles from Guard credentials.
|
|
116
|
+
|
|
117
|
+
Discovers AWS credentials for the given account, enumerates sub-accounts
|
|
118
|
+
via Organizations (if applicable), and writes ~/.aws/config profiles that
|
|
119
|
+
use guard as the credential_process.
|
|
120
|
+
|
|
121
|
+
The --account can be provided here or as the top-level guard --account flag.
|
|
122
|
+
|
|
123
|
+
\b
|
|
124
|
+
Example usages:
|
|
125
|
+
- guard --account chariot+client@praetorian.com access aws
|
|
126
|
+
- guard access aws --account chariot+client@praetorian.com
|
|
127
|
+
- guard access aws --account chariot+client@praetorian.com --prefix myclient
|
|
128
|
+
"""
|
|
129
|
+
import boto3
|
|
130
|
+
|
|
131
|
+
# Resolve account: explicit --account on this command, or inherited from guard --account
|
|
132
|
+
if account is None:
|
|
133
|
+
account = sdk.keychain.account
|
|
134
|
+
if account is None:
|
|
135
|
+
raise click.ClickException('--account is required. Provide it here or as guard --account.')
|
|
136
|
+
|
|
137
|
+
sdk.keychain.assume_role(account)
|
|
138
|
+
|
|
139
|
+
if prefix is None:
|
|
140
|
+
prefix = extract_prefix(account)
|
|
141
|
+
|
|
142
|
+
# List credentials and filter to AWS type
|
|
143
|
+
creds_response = sdk.credentials.list()
|
|
144
|
+
credentials_list = creds_response[0] if isinstance(creds_response, tuple) else []
|
|
145
|
+
|
|
146
|
+
aws_credentials = [c for c in credentials_list if c.get('type', '') == 'aws']
|
|
147
|
+
|
|
148
|
+
if not aws_credentials:
|
|
149
|
+
click.echo(f'No AWS credentials found for account {account}')
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
all_profiles = []
|
|
153
|
+
|
|
154
|
+
for cred in aws_credentials:
|
|
155
|
+
credential_id = cred.get('credentialId', '')
|
|
156
|
+
account_key = cred.get('accountKey', '')
|
|
157
|
+
category = cred.get('category', 'env-integration')
|
|
158
|
+
root_account_id = extract_account_id(account_key)
|
|
159
|
+
|
|
160
|
+
# Fetch the root credential to get temp AWS creds
|
|
161
|
+
try:
|
|
162
|
+
result = sdk.credentials.get(credential_id, category, 'aws', ['token'])
|
|
163
|
+
except Exception as e:
|
|
164
|
+
click.echo(f'Warning: failed to get credential {credential_id}: {e}')
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
access_key = result.get('credentialValue', {}).get('accessKeyId')
|
|
168
|
+
secret_key = result.get('credentialValue', {}).get('secretAccessKey')
|
|
169
|
+
session_token = result.get('credentialValue', {}).get('sessionToken')
|
|
170
|
+
|
|
171
|
+
if not access_key:
|
|
172
|
+
click.echo(f'Warning: credential {credential_id} returned no access key')
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Try Organizations to discover sub-accounts
|
|
176
|
+
sub_accounts = []
|
|
177
|
+
try:
|
|
178
|
+
org_client = boto3.client(
|
|
179
|
+
'organizations',
|
|
180
|
+
aws_access_key_id=access_key,
|
|
181
|
+
aws_secret_access_key=secret_key,
|
|
182
|
+
aws_session_token=session_token,
|
|
183
|
+
region_name='us-east-1'
|
|
184
|
+
)
|
|
185
|
+
paginator = org_client.get_paginator('list_accounts')
|
|
186
|
+
for page in paginator.paginate():
|
|
187
|
+
sub_accounts.extend(page['Accounts'])
|
|
188
|
+
except Exception:
|
|
189
|
+
# Not an org account — that's fine, just use the root
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
profiles = build_aws_config_profiles(
|
|
193
|
+
account_email=account,
|
|
194
|
+
prefix=prefix,
|
|
195
|
+
credential_id=credential_id,
|
|
196
|
+
root_account_id=root_account_id,
|
|
197
|
+
sub_accounts=sub_accounts,
|
|
198
|
+
category=category,
|
|
199
|
+
)
|
|
200
|
+
all_profiles.extend(profiles)
|
|
201
|
+
|
|
202
|
+
if not all_profiles:
|
|
203
|
+
click.echo(f'No AWS profiles could be generated for account {account}')
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
write_aws_config(all_profiles)
|
|
207
|
+
click.echo(f'Wrote {len(all_profiles)} AWS profile(s) to ~/.aws/config:')
|
|
208
|
+
for profile_name, _ in all_profiles:
|
|
209
|
+
click.echo(f' {profile_name}')
|
|
@@ -6,7 +6,7 @@ from praetorian_cli.sdk.keychain import Keychain, DEFAULT_API, DEFAULT_CLIENT_ID
|
|
|
6
6
|
@click.pass_context
|
|
7
7
|
def configure(click_context):
|
|
8
8
|
"""Configure the CLI with API key authentication."""
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
api_key_id = click.prompt("Enter your API Key ID")
|
|
11
11
|
api_key_secret = click.prompt("Enter your API Key secret", hide_input=True)
|
|
12
12
|
|
|
@@ -18,16 +18,17 @@ from praetorian_cli.handlers.add import add
|
|
|
18
18
|
@click.option('--type', 'customer_type', type=click.Choice(['ENGAGEMENT', 'MANAGED', 'SAAS', 'PILOT', 'FREEMIUM']),
|
|
19
19
|
default='ENGAGEMENT', show_default=True, help='Customer type')
|
|
20
20
|
@click.option('--collaborator', multiple=True, help='Additional collaborator emails (format: email:role)')
|
|
21
|
-
|
|
21
|
+
@click.option('--allowed-domain', multiple=True, help='Allowed email domains for collaborators (e.g., acme.com)')
|
|
22
|
+
def create_customer(sdk, email, display_name, scan_level, customer_type, collaborator, allowed_domain):
|
|
22
23
|
""" Create a new customer account on the Guard platform
|
|
23
24
|
|
|
24
25
|
Creates a Cognito user, Guard account, and adds default collaborators.
|
|
25
26
|
|
|
26
27
|
\b
|
|
27
28
|
Example usages:
|
|
28
|
-
- guard add customer --email ops@acme.com --name "ACME Corp"
|
|
29
|
-
- guard add customer --email ops@acme.com --name "ACME Corp" --type MANAGED
|
|
30
|
-
- guard add customer --email ops@acme.com --name "ACME Corp" --collaborator "analyst@praetorian.com:admin"
|
|
29
|
+
- guard add customer --email ops@acme.com --name "ACME Corp" --allowed-domain acme.com
|
|
30
|
+
- guard add customer --email ops@acme.com --name "ACME Corp" --type MANAGED --allowed-domain acme.com
|
|
31
|
+
- guard add customer --email ops@acme.com --name "ACME Corp" --allowed-domain acme.com --collaborator "analyst@praetorian.com:admin"
|
|
31
32
|
"""
|
|
32
33
|
body = {
|
|
33
34
|
'username': email,
|
|
@@ -47,6 +48,8 @@ def create_customer(sdk, email, display_name, scan_level, customer_type, collabo
|
|
|
47
48
|
collabs.append({'email': c, 'role': 'admin'})
|
|
48
49
|
body['collaborators'] = collabs
|
|
49
50
|
|
|
51
|
+
body['allowed_domains'] = [d.lower().strip() for d in allowed_domain]
|
|
52
|
+
|
|
50
53
|
result = sdk.post('customer/onboard', body)
|
|
51
54
|
click.echo(f'Customer created: {email} ({display_name})')
|
|
52
55
|
print_json(result)
|
|
@@ -25,6 +25,8 @@ def export():
|
|
|
25
25
|
@click.option('--end-date', default='', help='Engagement end date (ISO format)')
|
|
26
26
|
@click.option('--report-date', default='', help='Report date (ISO format). Default: today')
|
|
27
27
|
@click.option('--draft/--no-draft', default=False, help='Add DRAFT watermark')
|
|
28
|
+
@click.option('--retest/--no-retest', default=False,
|
|
29
|
+
help='Include retest status badges and sections')
|
|
28
30
|
@click.option('--version', 'report_version', default='1.0', help='Report version string')
|
|
29
31
|
@click.option('--format', 'export_format', type=click.Choice(['pdf', 'zip']),
|
|
30
32
|
default='pdf', help='Export format')
|
|
@@ -45,7 +47,7 @@ def export():
|
|
|
45
47
|
@click.option('--no-download', is_flag=True, default=False,
|
|
46
48
|
help='Skip downloading the file; just print the job result')
|
|
47
49
|
def report(chariot, title, client_name, status_filter, risk_keys,
|
|
48
|
-
target, start_date, end_date, report_date, draft,
|
|
50
|
+
target, start_date, end_date, report_date, draft, retest,
|
|
49
51
|
report_version, export_format, group_by, shared,
|
|
50
52
|
executive_summary, narratives, appendix, output,
|
|
51
53
|
timeout, no_download):
|
|
@@ -80,6 +82,7 @@ def report(chariot, title, client_name, status_filter, risk_keys,
|
|
|
80
82
|
end_date=end_date,
|
|
81
83
|
report_date=report_date,
|
|
82
84
|
draft=draft,
|
|
85
|
+
retest=retest,
|
|
83
86
|
version=report_version,
|
|
84
87
|
export_format=export_format,
|
|
85
88
|
group_by=group_by,
|
|
@@ -99,7 +99,12 @@ def integrations(chariot, filter, details, offset, page):
|
|
|
99
99
|
|
|
100
100
|
@list.command()
|
|
101
101
|
@list_params('group of the job asset')
|
|
102
|
-
|
|
102
|
+
@click.option('-c', '--capability', default=None, help='Filter by capability name (e.g., nuclei, portscan, diana-agent)')
|
|
103
|
+
@click.option('-s', '--status', type=click.Choice(['JQ', 'JR', 'JP', 'JF']),
|
|
104
|
+
default=None, help='Filter by job status (JQ=queued, JR=running, JP=passed, JF=failed)')
|
|
105
|
+
@click.option('-t', '--target', default=None, help='Filter by target DNS or hostname')
|
|
106
|
+
@click.option('--summary', is_flag=True, default=False, help='Show job count summary by capability and status')
|
|
107
|
+
def jobs(chariot, capability, status, target, summary, filter, details, offset, page):
|
|
103
108
|
"""
|
|
104
109
|
List jobs
|
|
105
110
|
|
|
@@ -110,10 +115,24 @@ def jobs(chariot, filter, details, offset, page):
|
|
|
110
115
|
Example usages:
|
|
111
116
|
- guard list jobs
|
|
112
117
|
- guard list jobs --filter www.example.com
|
|
118
|
+
- guard list jobs --capability nuclei
|
|
119
|
+
- guard list jobs --capability diana-agent --details
|
|
120
|
+
- guard list jobs --status JR
|
|
121
|
+
- guard list jobs --target api.example.com
|
|
122
|
+
- guard list jobs --summary
|
|
113
123
|
- guard list jobs --details
|
|
114
124
|
- guard list jobs --page all
|
|
115
125
|
"""
|
|
116
|
-
|
|
126
|
+
if summary:
|
|
127
|
+
print_json(chariot.jobs.summary(pagination_size(page)))
|
|
128
|
+
elif capability:
|
|
129
|
+
render_list_results(chariot.jobs.list_by_capability(capability, offset, pagination_size(page)), details)
|
|
130
|
+
elif status:
|
|
131
|
+
render_list_results(chariot.jobs.list_by_status(status, offset, pagination_size(page)), details)
|
|
132
|
+
elif target:
|
|
133
|
+
render_list_results(chariot.jobs.list_by_target(target, offset, pagination_size(page)), details)
|
|
134
|
+
else:
|
|
135
|
+
render_list_results(chariot.jobs.list(filter, offset, pagination_size(page)), details)
|
|
117
136
|
|
|
118
137
|
|
|
119
138
|
@list.command()
|
|
@@ -175,26 +175,109 @@ def tool(sdk, tool_name, target, extra_config, credential, wait, use_agent, loca
|
|
|
175
175
|
|
|
176
176
|
@run.command('list')
|
|
177
177
|
@cli_handler
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
@click.option('-s', '--surface', default='', help='Filter by surface (external, internal, cloud, repository, application)')
|
|
179
|
+
@click.option('-e', '--executor', default='', help='Filter by executor (chariot, janus, aegis, aurelian, agent)')
|
|
180
|
+
@click.option('--json', 'as_json', is_flag=True, default=False, help='Output as JSON')
|
|
181
|
+
def list_tools(sdk, surface, executor, as_json):
|
|
182
|
+
""" List all available tools and capabilities
|
|
180
183
|
|
|
181
184
|
\b
|
|
182
|
-
|
|
185
|
+
Shows all Guard capabilities with descriptions, indicating which can
|
|
186
|
+
be installed locally. Use --surface or --executor to filter.
|
|
187
|
+
|
|
188
|
+
\b
|
|
189
|
+
Example usages:
|
|
183
190
|
guard run list
|
|
191
|
+
guard run list --surface external
|
|
192
|
+
guard run list --executor agent
|
|
193
|
+
guard run list --json
|
|
184
194
|
"""
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
from praetorian_cli.runners.local import INSTALLABLE_TOOLS, list_installed
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result = sdk.capabilities.list()
|
|
199
|
+
caps = result[0] if isinstance(result, tuple) else result
|
|
200
|
+
if not isinstance(caps, list):
|
|
201
|
+
caps = []
|
|
202
|
+
except Exception:
|
|
203
|
+
caps = []
|
|
204
|
+
|
|
205
|
+
if not caps:
|
|
206
|
+
click.echo('Could not fetch capabilities from backend.', err=True)
|
|
207
|
+
click.echo(f'\nLocally installable tools:')
|
|
208
|
+
for name, info in sorted(INSTALLABLE_TOOLS.items()):
|
|
209
|
+
click.echo(f' {name:<18} {info["description"]}')
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if as_json:
|
|
213
|
+
print_json(caps)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
installed = list_installed()
|
|
217
|
+
|
|
218
|
+
# Apply filters
|
|
219
|
+
if surface:
|
|
220
|
+
caps = [c for c in caps if c.get('surface', '') == surface]
|
|
221
|
+
if executor:
|
|
222
|
+
caps = [c for c in caps if c.get('executor', '') == executor]
|
|
223
|
+
|
|
224
|
+
if not caps:
|
|
225
|
+
click.echo('No capabilities match the filters.')
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Sort: installable first, then by name
|
|
229
|
+
def sort_key(c):
|
|
230
|
+
name = c.get('name', '')
|
|
231
|
+
is_installable = name in INSTALLABLE_TOOLS
|
|
232
|
+
return (not is_installable, name)
|
|
233
|
+
|
|
234
|
+
caps.sort(key=sort_key)
|
|
235
|
+
|
|
236
|
+
# Render table
|
|
237
|
+
click.echo(f'\n{"Name":<22} {"Type":<10} {"Surface":<12} {"Description"}')
|
|
238
|
+
click.echo(f'{"─"*22} {"─"*10} {"─"*12} {"─"*48}')
|
|
239
|
+
|
|
240
|
+
for c in caps:
|
|
241
|
+
name = c.get('name', '')
|
|
242
|
+
desc = c.get('description', '') or c.get('title', '')
|
|
243
|
+
cap_surface = c.get('surface', '')
|
|
244
|
+
cap_executor = c.get('executor', '')
|
|
245
|
+
|
|
246
|
+
# Determine type label
|
|
247
|
+
if name in INSTALLABLE_TOOLS:
|
|
248
|
+
if name in installed:
|
|
249
|
+
type_label = 'local ✓'
|
|
250
|
+
else:
|
|
251
|
+
type_label = 'local'
|
|
252
|
+
elif cap_executor == 'agent':
|
|
253
|
+
type_label = 'agent'
|
|
254
|
+
else:
|
|
255
|
+
type_label = 'remote'
|
|
256
|
+
|
|
257
|
+
# Truncate description
|
|
258
|
+
max_desc = 55
|
|
259
|
+
if len(desc) > max_desc:
|
|
260
|
+
desc = desc[:max_desc - 1] + '…'
|
|
261
|
+
|
|
262
|
+
click.echo(f'{name:<22} {type_label:<10} {cap_surface:<12} {desc}')
|
|
263
|
+
|
|
264
|
+
click.echo(f'\n{len(caps)} capabilities')
|
|
265
|
+
installable_count = sum(1 for c in caps if c.get('name', '') in INSTALLABLE_TOOLS)
|
|
266
|
+
installed_count = sum(1 for c in caps if c.get('name', '') in installed)
|
|
267
|
+
if installable_count:
|
|
268
|
+
click.echo(f'{installable_count} installable locally ({installed_count} installed) — use "guard run install <name>"')
|
|
190
269
|
|
|
191
270
|
|
|
192
271
|
@run.command('capabilities')
|
|
193
272
|
@cli_handler
|
|
194
273
|
@click.option('-n', '--name', default='', help='Filter by capability name')
|
|
195
274
|
@click.option('-t', '--target', default='', help='Filter by target type')
|
|
196
|
-
|
|
197
|
-
|
|
275
|
+
@click.option('-e', '--executor', default='', help='Filter by executor')
|
|
276
|
+
def capabilities(sdk, name, target, executor):
|
|
277
|
+
""" List all capabilities as JSON (for scripting)
|
|
278
|
+
|
|
279
|
+
\b
|
|
280
|
+
Full machine-readable output. For a human-readable view, use "guard run list".
|
|
198
281
|
|
|
199
282
|
\b
|
|
200
283
|
Example usages:
|
|
@@ -202,8 +285,9 @@ def capabilities(sdk, name, target):
|
|
|
202
285
|
guard run capabilities --name nuclei
|
|
203
286
|
guard run capabilities --target asset
|
|
204
287
|
"""
|
|
205
|
-
result = sdk.capabilities.list(name=name, target=target)
|
|
206
|
-
|
|
288
|
+
result = sdk.capabilities.list(name=name, target=target, executor=executor)
|
|
289
|
+
caps = result[0] if isinstance(result, tuple) else result
|
|
290
|
+
print_json(caps)
|
|
207
291
|
|
|
208
292
|
|
|
209
293
|
@run.command('install')
|
|
@@ -71,7 +71,10 @@ def is_installed(tool_name: str) -> bool:
|
|
|
71
71
|
def install_tool(tool_name: str, force=False) -> str:
|
|
72
72
|
"""Download and install a tool from GitHub releases. Returns binary path."""
|
|
73
73
|
if tool_name not in INSTALLABLE_TOOLS:
|
|
74
|
-
|
|
74
|
+
lines = [f'Unknown tool: {tool_name}. Installable tools:']
|
|
75
|
+
for name in sorted(INSTALLABLE_TOOLS):
|
|
76
|
+
lines.append(f' {name:<18} {INSTALLABLE_TOOLS[name]["description"]}')
|
|
77
|
+
raise ValueError('\n'.join(lines))
|
|
75
78
|
|
|
76
79
|
if not force and is_installed(tool_name):
|
|
77
80
|
return get_binary_path(tool_name)
|