praetorian-cli 2.4.1__tar.gz → 2.4.3__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.1/praetorian_cli.egg-info → praetorian_cli-2.4.3}/PKG-INFO +1 -1
- praetorian_cli-2.4.3/praetorian_cli/handlers/access.py +209 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/add.py +50 -19
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/configure.py +1 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/delete.py +58 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/engagement.py +7 -4
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/export.py +7 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/list.py +21 -2
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/run.py +36 -10
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/update.py +6 -3
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/utils.py +16 -14
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/main.py +1 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/runners/local.py +107 -22
- praetorian_cli-2.4.3/praetorian_cli/sdk/entities/account_discovery.py +443 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/assets.py +32 -2
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/attributes.py +15 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/credentials.py +46 -6
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/jobs.py +171 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/reports.py +13 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/risks.py +24 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_access_aws.py +198 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_bulk.py +315 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_credential_process.py +63 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_credentials.py +266 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_export_cli.py +100 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_job.py +44 -0
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_local_runner.py +230 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_report.py +11 -1
- praetorian_cli-2.4.3/praetorian_cli/sdk/test/test_run_cli.py +128 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_z_cli.py +2 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/ui_mocks.py +5 -0
- praetorian_cli-2.4.3/praetorian_cli/ui/aegis/account_selector.py +250 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/job.py +2 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/job_helpers.py +19 -4
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/list.py +1 -3
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/schedule.py +262 -166
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/set.py +27 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/menu.py +120 -19
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/__init__.py +1 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/accounts.py +6 -2
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/context.py +70 -2
- praetorian_cli-2.4.3/praetorian_cli/ui/console/commands/data.py +162 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/marcus.py +109 -14
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/tools.py +156 -31
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/console.py +28 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/context.py +1 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3/praetorian_cli.egg-info}/PKG-INFO +1 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli.egg-info/SOURCES.txt +11 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/setup.cfg +1 -1
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/LICENSE +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/MANIFEST.in +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/README.md +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/aegis.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/agent.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/critfinder.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/find.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/get.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/report.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/ssh_utils.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/runners/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/chariot.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/accounts.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/ad.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/aegis.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/scanners.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/schedules.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/schema.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/webhook.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/entities/webpage.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/guard.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/mcp_server.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/model/aegis.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_asset.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_conversation.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_mcp.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_risk.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_search.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/test_webpage.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/sdk/test/utils.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/help.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/info.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/constants.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/theme.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/aegis/utils.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/reporting.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/commands/search.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/console/renderer.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/conversation/__init__.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli.egg-info/requires.txt +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.4.1 → praetorian_cli-2.4.3}/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 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from praetorian_cli.handlers.chariot import chariot
|
|
8
8
|
from praetorian_cli.handlers.cli_decorators import cli_handler, praetorian_only
|
|
9
|
-
from praetorian_cli.handlers.utils import error, parse_configuration_value
|
|
9
|
+
from praetorian_cli.handlers.utils import error, parse_configuration_value, parse_kv_entries
|
|
10
10
|
from praetorian_cli.sdk.model.globals import AddRisk, Asset, Seed, Kind
|
|
11
11
|
|
|
12
12
|
|
|
@@ -357,40 +357,71 @@ def webpage(sdk, url, parent):
|
|
|
357
357
|
sdk.webpage.add(url, parent)
|
|
358
358
|
|
|
359
359
|
|
|
360
|
-
@add.
|
|
360
|
+
@add.group(invoke_without_command=True)
|
|
361
361
|
@cli_handler
|
|
362
|
-
@click.option('-r', '--resource-key',
|
|
363
|
-
@click.option('-c', '--category',
|
|
362
|
+
@click.option('-r', '--resource-key', help='The resource key for the credential (e.g., account key)')
|
|
363
|
+
@click.option('-c', '--category',
|
|
364
364
|
type=click.Choice(['integration', 'cloud', 'env-integration']),
|
|
365
365
|
help='The category of the credential')
|
|
366
|
-
@click.option('-t', '--type', 'cred_type',
|
|
367
|
-
help='The type of credential (aws, gcp, azure, static,
|
|
368
|
-
|
|
366
|
+
@click.option('-t', '--type', 'cred_type',
|
|
367
|
+
help='The type of credential (aws, gcp, azure, static-token, ssh-key, '
|
|
368
|
+
'json-credential, active-directory, burp-authentication, web-auth, etc.)')
|
|
369
|
+
@click.option('-l', '--label', help='A human-readable label for the credential')
|
|
369
370
|
@click.option('-p', '--param', 'parameters', multiple=True,
|
|
370
371
|
help='Parameter in format key=value (can be specified multiple times)')
|
|
371
|
-
|
|
372
|
+
@click.pass_context
|
|
373
|
+
def credential(ctx, sdk, resource_key, category, cred_type, label, parameters):
|
|
372
374
|
""" Add a credential
|
|
373
375
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
+
Without a subcommand, adds a credential generically — you provide all of category,
|
|
377
|
+
type, label, and params. Use the typed subcommands (e.g. `webauth`) for friendlier
|
|
378
|
+
UX when adding a known credential type.
|
|
376
379
|
|
|
377
380
|
\b
|
|
378
381
|
Example usages:
|
|
379
382
|
- guard add credential --resource-key "C.0c6cf7104f516b08-OGMPG" --category env-integration --type active-directory --label "Robb Stark" --param username=robb.stark --param password=sexywolfy --param domain=north.sevenkingdoms.local
|
|
380
383
|
- guard add credential -r "C.example-key" -c cloud -t aws --label "AWS Production" -p region=us-east-1 -p role_arn=arn:aws:iam::123456789012:role/MyRole
|
|
381
|
-
- guard add credential -r "C.example-key" -c integration -t static --label "API Token" -p token=abc123xyz
|
|
384
|
+
- guard add credential -r "C.example-key" -c integration -t static-token --label "API Token" -p token=abc123xyz
|
|
382
385
|
"""
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
if ctx.invoked_subcommand is not None:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
missing = [name for name, val in
|
|
390
|
+
[('--resource-key', resource_key), ('--category', category),
|
|
391
|
+
('--type', cred_type), ('--label', label)] if not val]
|
|
392
|
+
if missing:
|
|
393
|
+
error(f"Missing required option(s): {', '.join(missing)}")
|
|
394
|
+
|
|
395
|
+
params = parse_kv_entries(parameters, label='Param')
|
|
391
396
|
|
|
392
397
|
try:
|
|
393
398
|
result = sdk.credentials.add(resource_key, category, cred_type, label, params)
|
|
394
399
|
click.echo(json.dumps(result, indent=2))
|
|
395
400
|
except Exception as e:
|
|
396
401
|
error(f'Unable to add credential. Error: {e}')
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@credential.command('webauth')
|
|
405
|
+
@cli_handler
|
|
406
|
+
@click.option('-k', '--resource-key', required=True,
|
|
407
|
+
help='Web-application key (e.g., #webapplication#https://app.example.com)')
|
|
408
|
+
@click.option('-l', '--label', required=True, help='A human-readable label for the credential')
|
|
409
|
+
@click.option('-H', '--header', 'headers', multiple=True, required=True,
|
|
410
|
+
help='Header in format Name=Value (can be specified multiple times)')
|
|
411
|
+
def credential_webauth(sdk, resource_key, label, headers):
|
|
412
|
+
""" Add a static-token web-auth credential to a WebApplication.
|
|
413
|
+
|
|
414
|
+
Persists a header bag (e.g. an Authorization bearer token) that Chariot
|
|
415
|
+
capabilities and Burp Enterprise will use when scanning the target.
|
|
416
|
+
|
|
417
|
+
\b
|
|
418
|
+
Example usage:
|
|
419
|
+
- guard add credential webauth -k "#webapplication#https://app.example.com" -l "Prod token" -H "Authorization=Bearer abc123" -H "X-Tenant=acme"
|
|
420
|
+
"""
|
|
421
|
+
headers_dict = parse_kv_entries(headers, label='Header')
|
|
422
|
+
parameters = {'method': 'static-token', 'headers': headers_dict}
|
|
423
|
+
try:
|
|
424
|
+
result = sdk.credentials.add(resource_key, 'env-integration', 'web-auth', label, parameters)
|
|
425
|
+
click.echo(json.dumps(result, indent=2))
|
|
426
|
+
except Exception as e:
|
|
427
|
+
error(f'Unable to add web-auth credential. Error: {e}')
|
|
@@ -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
|
|
|
@@ -193,4 +193,61 @@ def webpage(chariot, key):
|
|
|
193
193
|
Arguments:
|
|
194
194
|
- KEY: the key of an existing webpage
|
|
195
195
|
"""
|
|
196
|
-
chariot.webpage.delete(key)
|
|
196
|
+
chariot.webpage.delete(key)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@delete.group()
|
|
200
|
+
def credential():
|
|
201
|
+
""" Delete a credential via the broker
|
|
202
|
+
|
|
203
|
+
For web-auth credentials shared across multiple WebApplications, this only
|
|
204
|
+
detaches the credential from the given resource_key — the underlying
|
|
205
|
+
credential is fully removed once it has no remaining attachments.
|
|
206
|
+
|
|
207
|
+
Use the typed subcommands (e.g. `webauth`) for friendlier UX, or `generic`
|
|
208
|
+
when deleting a credential type that doesn't have a typed subcommand yet.
|
|
209
|
+
"""
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@credential.command('generic')
|
|
214
|
+
@cli_handler
|
|
215
|
+
@click.argument('credential_id', required=True)
|
|
216
|
+
@click.option('-r', '--resource-key', required=True,
|
|
217
|
+
help='The resource key the credential is attached to (e.g., web-application key, account key)')
|
|
218
|
+
@click.option('-t', '--type', 'cred_type', required=True,
|
|
219
|
+
help='The credential type (e.g., active-directory, burp-authentication)')
|
|
220
|
+
def credential_generic(chariot, credential_id, resource_key, cred_type):
|
|
221
|
+
""" Delete a credential of any type (escape hatch).
|
|
222
|
+
|
|
223
|
+
\b
|
|
224
|
+
Arguments:
|
|
225
|
+
- CREDENTIAL_ID: the ID of the credential to delete
|
|
226
|
+
|
|
227
|
+
\b
|
|
228
|
+
Example usage:
|
|
229
|
+
- guard delete credential generic def-456 -r "#asset#example.com#1.2.3.4" -t active-directory
|
|
230
|
+
"""
|
|
231
|
+
chariot.credentials.delete(credential_id, resource_key, cred_type)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@credential.command('webauth')
|
|
235
|
+
@cli_handler
|
|
236
|
+
@click.argument('credential_id', required=True)
|
|
237
|
+
@click.option('-k', '--resource-key', required=True,
|
|
238
|
+
help='Web-application key (e.g., #webapplication#https://app.example.com)')
|
|
239
|
+
def credential_webauth(chariot, credential_id, resource_key):
|
|
240
|
+
""" Delete a web-auth credential from a WebApplication.
|
|
241
|
+
|
|
242
|
+
Detaches the credential from this WebApplication; the underlying credential
|
|
243
|
+
is fully removed once it has no remaining attachments.
|
|
244
|
+
|
|
245
|
+
\b
|
|
246
|
+
Arguments:
|
|
247
|
+
- CREDENTIAL_ID: the ID of the web-auth credential to delete
|
|
248
|
+
|
|
249
|
+
\b
|
|
250
|
+
Example usage:
|
|
251
|
+
- guard delete credential webauth abc-123 -k "#webapplication#https://app.example.com"
|
|
252
|
+
"""
|
|
253
|
+
chariot.credentials.delete(credential_id, resource_key, 'web-auth')
|
|
@@ -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)
|
|
@@ -28,6 +28,9 @@ def export():
|
|
|
28
28
|
@click.option('--retest/--no-retest', default=False,
|
|
29
29
|
help='Include retest status badges and sections')
|
|
30
30
|
@click.option('--version', 'report_version', default='1.0', help='Report version string')
|
|
31
|
+
@click.option('--sow', default='', help='Statement of Work identifier (expands %%SOW%% shortcode).')
|
|
32
|
+
@click.option('--footer', default='', help='Custom page-footer text (overrides the report title when set).')
|
|
33
|
+
@click.option('--confidential-label', 'confidential_label', default='', help='Confidentiality label shown on cover, header, and footer (defaults to "Confidential").')
|
|
31
34
|
@click.option('--format', 'export_format', type=click.Choice(['pdf', 'zip']),
|
|
32
35
|
default='pdf', help='Export format')
|
|
33
36
|
@click.option('--group-by', type=click.Choice(['attack_surface', 'tag']),
|
|
@@ -48,7 +51,7 @@ def export():
|
|
|
48
51
|
help='Skip downloading the file; just print the job result')
|
|
49
52
|
def report(chariot, title, client_name, status_filter, risk_keys,
|
|
50
53
|
target, start_date, end_date, report_date, draft, retest,
|
|
51
|
-
report_version, export_format, group_by, shared,
|
|
54
|
+
report_version, sow, footer, confidential_label, export_format, group_by, shared,
|
|
52
55
|
executive_summary, narratives, appendix, output,
|
|
53
56
|
timeout, no_download):
|
|
54
57
|
""" Generate and download a PDF or ZIP report.
|
|
@@ -84,6 +87,9 @@ def report(chariot, title, client_name, status_filter, risk_keys,
|
|
|
84
87
|
draft=draft,
|
|
85
88
|
retest=retest,
|
|
86
89
|
version=report_version,
|
|
90
|
+
sow=sow,
|
|
91
|
+
footer=footer,
|
|
92
|
+
confidential_label=confidential_label,
|
|
87
93
|
export_format=export_format,
|
|
88
94
|
group_by=group_by,
|
|
89
95
|
shared_output=shared,
|
|
@@ -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()
|
|
@@ -124,27 +124,34 @@ def run():
|
|
|
124
124
|
pass
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
@run.command(
|
|
127
|
+
@run.command(
|
|
128
|
+
'tool',
|
|
129
|
+
context_settings={'ignore_unknown_options': True, 'allow_extra_args': True},
|
|
130
|
+
)
|
|
128
131
|
@cli_handler
|
|
129
132
|
@click.argument('tool_name')
|
|
130
133
|
@click.argument('target')
|
|
134
|
+
@click.argument('tool_args', nargs=-1, type=click.UNPROCESSED)
|
|
131
135
|
@click.option('-c', '--config', 'extra_config', default='', help='Extra JSON config to merge')
|
|
132
136
|
@click.option('--credential', multiple=True, help='Credential ID(s) to use')
|
|
133
137
|
@click.option('--wait', is_flag=True, default=False, help='Wait for job completion and show results')
|
|
134
138
|
@click.option('--ask', 'use_agent', is_flag=True, default=False, help='Run via Marcus AI agent instead of direct job')
|
|
135
139
|
@click.option('--local', is_flag=True, default=False, help='Run locally using installed binary')
|
|
136
140
|
@click.option('--remote', is_flag=True, default=False, help='Force remote job execution on Guard backend')
|
|
137
|
-
def tool(sdk, tool_name, target, extra_config, credential, wait, use_agent, local, remote):
|
|
141
|
+
def tool(sdk, tool_name, target, tool_args, extra_config, credential, wait, use_agent, local, remote):
|
|
138
142
|
""" Execute a named security tool against a target
|
|
139
143
|
|
|
140
144
|
By default, runs LOCALLY if the binary is installed, otherwise schedules
|
|
141
|
-
a remote job. Use --local or --remote to force.
|
|
145
|
+
a remote job. Use --local or --remote to force. Any additional arguments
|
|
146
|
+
after the target are forwarded to the local binary. Use `--` to pass
|
|
147
|
+
flags that collide with our own options.
|
|
142
148
|
|
|
143
149
|
\b
|
|
144
150
|
Example usages:
|
|
145
|
-
guard run tool brutus 10.0.1.5
|
|
146
|
-
guard run tool
|
|
147
|
-
guard run tool
|
|
151
|
+
guard run tool brutus 10.0.1.5:22
|
|
152
|
+
guard run tool brutus 10.0.1.5:22 --protocol ssh -U users.txt
|
|
153
|
+
guard run tool brutus 10.0.1.5:22 -- --wait
|
|
154
|
+
guard run tool nuclei example.com --remote -c '{"templates":"cves/"}'
|
|
148
155
|
"""
|
|
149
156
|
from praetorian_cli.runners.local import is_installed as _is_installed
|
|
150
157
|
|
|
@@ -152,11 +159,26 @@ def tool(sdk, tool_name, target, extra_config, credential, wait, use_agent, loca
|
|
|
152
159
|
if not cap:
|
|
153
160
|
error(f'Unknown capability: {tool_name}. Use "guard run capabilities" to see available capabilities.')
|
|
154
161
|
|
|
155
|
-
#
|
|
162
|
+
# --local / --remote / --ask are mutually exclusive routing flags.
|
|
163
|
+
mode_flags = [('--local', local), ('--remote', remote), ('--ask', use_agent)]
|
|
164
|
+
set_flags = [name for name, v in mode_flags if v]
|
|
165
|
+
if len(set_flags) > 1:
|
|
166
|
+
error(f'Mutually exclusive flags: {", ".join(set_flags)}. Choose one.')
|
|
167
|
+
|
|
168
|
+
tool_args = list(tool_args or [])
|
|
169
|
+
|
|
170
|
+
# Decide local vs remote first so we can validate tool_args against the resolved path.
|
|
156
171
|
run_local = local or (not remote and not use_agent and _is_installed(tool_name.lower()))
|
|
157
172
|
|
|
173
|
+
if tool_args and (remote or use_agent or not run_local):
|
|
174
|
+
error(
|
|
175
|
+
'Extra arguments after the target are only supported when running locally. '
|
|
176
|
+
'Install the tool locally ("guard run install %s") or encode settings as JSON '
|
|
177
|
+
'via -c \'{"key":"value"}\'.' % tool_name.lower()
|
|
178
|
+
)
|
|
179
|
+
|
|
158
180
|
if run_local:
|
|
159
|
-
_run_local(sdk, tool_name.lower(), target, extra_config)
|
|
181
|
+
_run_local(sdk, tool_name.lower(), target, extra_config, tool_args)
|
|
160
182
|
elif use_agent:
|
|
161
183
|
target_key, warning = resolve_target(sdk, target, cap['target_type'])
|
|
162
184
|
if not target_key:
|
|
@@ -376,7 +398,7 @@ def _run_via_agent(sdk, cap, target_key):
|
|
|
376
398
|
error(str(e))
|
|
377
399
|
|
|
378
400
|
|
|
379
|
-
def _run_local(sdk, tool_name, target, extra_config):
|
|
401
|
+
def _run_local(sdk, tool_name, target, extra_config, tool_args=None):
|
|
380
402
|
"""Run a tool locally using the installed binary."""
|
|
381
403
|
from praetorian_cli.runners.local import LocalRunner, get_tool_plugin
|
|
382
404
|
|
|
@@ -392,7 +414,7 @@ def _run_local(sdk, tool_name, target, extra_config):
|
|
|
392
414
|
raw_target = parts[-1] if len(parts) > 3 else parts[2] if len(parts) > 2 else target
|
|
393
415
|
|
|
394
416
|
plugin = get_tool_plugin(tool_name)
|
|
395
|
-
args = plugin.build_args(raw_target, extra_config)
|
|
417
|
+
args = plugin.build_args(raw_target, extra_config, pass_through=list(tool_args or []))
|
|
396
418
|
|
|
397
419
|
click.echo(f'Running {tool_name} locally against {raw_target}...')
|
|
398
420
|
click.echo(f'Command: {tool_name} {" ".join(args)}')
|
|
@@ -408,6 +430,10 @@ def _run_local(sdk, tool_name, target, extra_config):
|
|
|
408
430
|
proc.wait(timeout=600)
|
|
409
431
|
except subprocess.TimeoutExpired:
|
|
410
432
|
proc.kill()
|
|
433
|
+
try:
|
|
434
|
+
proc.wait(timeout=5)
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
411
437
|
click.echo('\nTimed out (10 min).', err=True)
|
|
412
438
|
|
|
413
439
|
stderr = proc.stderr.read() if proc.stderr else ''
|
|
@@ -16,8 +16,10 @@ def update():
|
|
|
16
16
|
@click.argument('key', required=True)
|
|
17
17
|
@click.option('-s', '--status', type=click.Choice([s.value for s in Asset]), help='The status of the asset')
|
|
18
18
|
@click.option('-f', '--surface', required=False, default='', help=f'Attack surface of the asset', show_default=False)
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
@click.option('--secret', default=None,
|
|
20
|
+
help='For WebApplication assets, the credential ID to set as the default credential')
|
|
21
|
+
def asset(chariot, key, status, surface, secret):
|
|
22
|
+
""" Update the status, surface, or default credential of an asset
|
|
21
23
|
|
|
22
24
|
\b
|
|
23
25
|
Argument:
|
|
@@ -27,8 +29,9 @@ def asset(chariot, key, status, surface):
|
|
|
27
29
|
Example usages:
|
|
28
30
|
- guard update asset "#asset#www.example.com#1.2.3.4" -s F
|
|
29
31
|
- guard update asset "#asset#www.example.com#1.2.3.4" -f internal
|
|
32
|
+
- guard update asset "#webapplication#https://app.example.com" --secret abc-123
|
|
30
33
|
"""
|
|
31
|
-
chariot.assets.update(key, status, surface)
|
|
34
|
+
chariot.assets.update(key, status, surface, secret=secret)
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
@update.command()
|