praetorian-cli 2.6.2__tar.gz → 2.6.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.6.2/praetorian_cli.egg-info → praetorian_cli-2.6.3}/PKG-INFO +15 -2
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/README.md +12 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/add.py +28 -3
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/aegis.py +52 -11
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/update.py +2 -8
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/chariot.py +2 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/aegis.py +165 -19
- praetorian_cli-2.6.3/praetorian_cli/sdk/entities/apks.py +53 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/risks.py +12 -57
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/mcp_server.py +101 -52
- praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_aegis_job_cli.py +120 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_agent.py +1 -1
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_bulk.py +1 -4
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_mcp.py +65 -0
- praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_mcp_mobile_tools.py +105 -0
- praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_mobile_dispatch.py +288 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_risk.py +31 -29
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_search.py +1 -1
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/utils.py +0 -1
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/data.py +1 -2
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3/praetorian_cli.egg-info}/PKG-INFO +15 -2
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/SOURCES.txt +4 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/requires.txt +2 -1
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/setup.cfg +3 -2
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/LICENSE +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/MANIFEST.in +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/access.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/agent.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/configure.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/critfinder.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/engagement.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/export.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/find.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/get.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/hunt.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/list.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/query.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/report.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/run.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/ssh_utils.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/vm.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/vm_proxy.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/main.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/runners/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/runners/local.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/account_discovery.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/accounts.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/ad.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/assets.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/attributes.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/conversations.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/credentials.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/endpoint_executions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/engineer_vm.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/hunts.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/jobs.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/reports.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/scanners.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/schedules.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/schema.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/webhook.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/webpage.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/exceptions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/guard.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/aegis.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/vm.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_access_aws.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_access_github.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_add_file_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_endpoint_live.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_management_users.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_network_policy.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_tunnel.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_v2_enrollment.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_agent_interactions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_asset.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_async_http.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_cli_errors.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation_approval_surfaces.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation_interactions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_credential_process.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_credentials.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_approvals.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_execution_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_executions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_network_policy.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_status.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_engineer_vm.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_entity_resolution_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_entity_resolver.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_export_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_file_verified_download.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_all_surface_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_chat.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_chat_live.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_data.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_internal_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_launch.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_memory_browser.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_open.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_overview.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_scope_selector.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_workflows.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunts_internal.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_job.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_keychain.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_keychain_cache.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_local_runner.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_my_by_raw_query.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_report.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_run_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_sdk_exceptions.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_terminal.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_test_handler.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_test_target.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_update_check.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_webpage.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_z_cli.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/account_selector.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/enrollment.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/help.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/hunt.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/info.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job_v2.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/list.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/network_policy.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/set.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/tunnel.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/user.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/constants.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/menu.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/theme.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/utils.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/async_http.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/accounts.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/context.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/marcus.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/reporting.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/search.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/tools.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/console.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/context.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/renderer.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/__init__.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/approvals.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/endpoint_status.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/entity_resolver.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/entity_selector.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_chat.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_chat_live.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_data.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_defaults.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_format.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_launch.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_memory.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_open.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_overview.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_workflows.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/terminal.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/textual_refresh.py +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: praetorian-cli
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.3
|
|
4
4
|
Summary: For interacting with the Guard API
|
|
5
5
|
Home-page: https://github.com/praetorian-inc/praetorian-cli
|
|
6
6
|
Author: Praetorian
|
|
@@ -16,7 +16,8 @@ Requires-Dist: boto3>=1.34.0
|
|
|
16
16
|
Requires-Dist: requests>=2.31.0
|
|
17
17
|
Requires-Dist: truststore>=0.10.0
|
|
18
18
|
Requires-Dist: pytest>=8.0.2
|
|
19
|
-
Requires-Dist: mcp>=
|
|
19
|
+
Requires-Dist: mcp>=2.0.0
|
|
20
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
20
21
|
Requires-Dist: anyio>=3.0.0
|
|
21
22
|
Requires-Dist: aiohttp>=3.9.0
|
|
22
23
|
Requires-Dist: textual>=0.47.0
|
|
@@ -529,8 +530,20 @@ guard run installed
|
|
|
529
530
|
guard engagement list
|
|
530
531
|
guard engagement create-customer --email ops@acme.com --name "ACME Corp"
|
|
531
532
|
guard engagement create-vault --client acme --sow SOW-1234 --sku WAPT --github-user jdoe
|
|
533
|
+
|
|
534
|
+
# Mobile testing: upload an APK, then run a capability against it on a device
|
|
535
|
+
guard add apk --file ./bank-app.apk
|
|
536
|
+
guard aegis list --details
|
|
537
|
+
guard aegis job --package com.bank.app
|
|
538
|
+
guard aegis job --package com.bank.app -c android-device-survey -e <endpoint-id>
|
|
532
539
|
```
|
|
533
540
|
|
|
541
|
+
A mobile capability runs on an enrolled Aegis v2 endpoint, named with `--endpoint` from
|
|
542
|
+
the IDs in `guard aegis list --details`. The target is the application rather than the
|
|
543
|
+
device, so the findings are filed on the `#apk#<package>` asset. Pinning a job to a
|
|
544
|
+
specific endpoint is reserved for Praetorian users: for everyone else Guard drops the pin
|
|
545
|
+
and selects an endpoint automatically.
|
|
546
|
+
|
|
534
547
|
# Developers
|
|
535
548
|
|
|
536
549
|
Both CLI and SDK is open-source in this repository. The SDK is installed along with the `praetorian-cli`
|
|
@@ -502,8 +502,20 @@ guard run installed
|
|
|
502
502
|
guard engagement list
|
|
503
503
|
guard engagement create-customer --email ops@acme.com --name "ACME Corp"
|
|
504
504
|
guard engagement create-vault --client acme --sow SOW-1234 --sku WAPT --github-user jdoe
|
|
505
|
+
|
|
506
|
+
# Mobile testing: upload an APK, then run a capability against it on a device
|
|
507
|
+
guard add apk --file ./bank-app.apk
|
|
508
|
+
guard aegis list --details
|
|
509
|
+
guard aegis job --package com.bank.app
|
|
510
|
+
guard aegis job --package com.bank.app -c android-device-survey -e <endpoint-id>
|
|
505
511
|
```
|
|
506
512
|
|
|
513
|
+
A mobile capability runs on an enrolled Aegis v2 endpoint, named with `--endpoint` from
|
|
514
|
+
the IDs in `guard aegis list --details`. The target is the application rather than the
|
|
515
|
+
device, so the findings are filed on the `#apk#<package>` asset. Pinning a job to a
|
|
516
|
+
specific endpoint is reserved for Praetorian users: for everyone else Guard drops the pin
|
|
517
|
+
and selects an endpoint automatically.
|
|
518
|
+
|
|
507
519
|
# Developers
|
|
508
520
|
|
|
509
521
|
Both CLI and SDK is open-source in this repository. The SDK is installed along with the `praetorian-cli`
|
|
@@ -195,11 +195,10 @@ def webhook(sdk):
|
|
|
195
195
|
help='Existing asset key, hostname, IP, or friendly name')
|
|
196
196
|
@click.option('-s', '--status', type=click.Choice([s.value for s in AddRisk]), required=True,
|
|
197
197
|
help=f'Status of the risk')
|
|
198
|
-
@click.option('-c', '--comment', default='', help='Comment for the risk')
|
|
199
198
|
@click.option('-cap', '--capability', default='', help='Capability that discoverd the risk')
|
|
200
199
|
@click.option('--title', '-t', default=None, help='Human-readable title for the risk')
|
|
201
200
|
@click.option('-g', '--tag', 'tags', multiple=True, help='Tag for the risk (can be specified multiple times)')
|
|
202
|
-
def risk(sdk, name, asset, status,
|
|
201
|
+
def risk(sdk, name, asset, status, capability, title, tags):
|
|
203
202
|
""" Add a risk
|
|
204
203
|
|
|
205
204
|
This command adds a risk to Guard. A risk must have an associated asset.
|
|
@@ -218,7 +217,33 @@ def risk(sdk, name, asset, status, comment, capability, title, tags):
|
|
|
218
217
|
- guard add risk CVE-2024-23049 --asset "#asset#example.com#1.2.3.4" --status TI --tag critical --tag needs-review
|
|
219
218
|
"""
|
|
220
219
|
asset_key = resolve_entity_reference(sdk, asset, 'asset')
|
|
221
|
-
sdk.risks.add(asset_key, name, status,
|
|
220
|
+
sdk.risks.add(asset_key, name, status, capability, title, tags)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@add.command()
|
|
224
|
+
@cli_handler
|
|
225
|
+
@click.option('-f', '--file', 'local_filepath', required=True,
|
|
226
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
227
|
+
help='Path to the local .apk file')
|
|
228
|
+
@click.option('-n', '--name', 'chariot_filepath',
|
|
229
|
+
help='Destination path in Guard storage. Defaults to the file name.')
|
|
230
|
+
def apk(sdk, local_filepath, chariot_filepath):
|
|
231
|
+
""" Upload an Android APK and register it as an asset
|
|
232
|
+
|
|
233
|
+
Uploads the APK and registers it, creating an asset keyed '#apk#<package>'. The
|
|
234
|
+
package name is read from the APK's own manifest rather than supplied, so
|
|
235
|
+
re-uploading a newer build of the same application updates that asset instead of
|
|
236
|
+
creating a second one.
|
|
237
|
+
|
|
238
|
+
\b
|
|
239
|
+
Example usages:
|
|
240
|
+
- guard add apk --file ./bank-app.apk
|
|
241
|
+
- guard add apk --file ./bank-app.apk --name engagements/acme/bank-app.apk
|
|
242
|
+
"""
|
|
243
|
+
result = sdk.apks.add(local_filepath, chariot_filepath)
|
|
244
|
+
key = result.get('key', '')
|
|
245
|
+
version = result.get('versionName', '')
|
|
246
|
+
click.echo(f"Registered {key}" + (f" (version {version})" if version else ""))
|
|
222
247
|
|
|
223
248
|
|
|
224
249
|
@add.command()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from praetorian_cli.handlers.chariot import chariot
|
|
3
3
|
from praetorian_cli.handlers.cli_decorators import cli_handler
|
|
4
|
+
from praetorian_cli.sdk.model.aegis import is_v2_agent
|
|
4
5
|
from praetorian_cli.ui.aegis.commands.enrollment import (
|
|
5
6
|
enrollment_error_message,
|
|
6
7
|
pending_enrollment_lines,
|
|
@@ -339,25 +340,62 @@ def _raise_network_policy_error(exc):
|
|
|
339
340
|
|
|
340
341
|
@aegis.command('job')
|
|
341
342
|
@cli_handler
|
|
342
|
-
@click.option('-c', '--capability', 'capabilities', multiple=True,
|
|
343
|
+
@click.option('-c', '--capability', 'capabilities', multiple=True,
|
|
344
|
+
help='Capability to run (e.g., windows-smb-snaffler, android-device-survey)')
|
|
343
345
|
@click.option('--config', help='JSON configuration string for the job')
|
|
344
|
-
@click.
|
|
346
|
+
@click.option('-p', '--package',
|
|
347
|
+
help='Android application ID to target, e.g. com.bank.app. Upload the APK first with "guard add apk".')
|
|
348
|
+
@click.option('-e', '--endpoint', 'endpoint_id',
|
|
349
|
+
help='Enrolled Aegis v2 endpoint to run the job on, as shown by "guard aegis list --details"')
|
|
350
|
+
@click.argument('client_id', required=False)
|
|
345
351
|
@click.pass_context
|
|
346
|
-
def job(ctx, sdk, capabilities, config, client_id):
|
|
347
|
-
"""Run a job on an Aegis agent
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
def job(ctx, sdk, capabilities, config, package, endpoint_id, client_id):
|
|
353
|
+
"""Run a job on an Aegis agent or an enrolled endpoint
|
|
354
|
+
|
|
355
|
+
Name the target with either CLIENT_ID, for an agent's own host, or --package, for
|
|
356
|
+
an Android application already registered with "guard add apk". Without -c, the
|
|
357
|
+
capabilities available for that target are listed instead of a job being run.
|
|
358
|
+
|
|
359
|
+
\b
|
|
360
|
+
Example usages:
|
|
361
|
+
- guard aegis job C.6e012b467f9faf82-OG9F0 -c linux-enum
|
|
362
|
+
- guard aegis job --package com.bank.app
|
|
363
|
+
- guard aegis job --package com.bank.app -c android-device-survey -e 16169bc5-7943-4783-af81-c4735616f7e9
|
|
364
|
+
- guard aegis job --package com.bank.app -c android-device-command -e 16169bc5-7943-4783-af81-c4735616f7e9 --config '{"command": "getprop ro.build.fingerprint"}'
|
|
365
|
+
"""
|
|
366
|
+
if client_id and package:
|
|
367
|
+
raise click.UsageError('A job has one target: pass either CLIENT_ID or --package, not both.')
|
|
368
|
+
|
|
369
|
+
if not client_id and not package:
|
|
370
|
+
raise click.UsageError('No target. Pass an agent CLIENT_ID, or --package to target an APK.')
|
|
371
|
+
|
|
372
|
+
agent = None
|
|
373
|
+
if client_id:
|
|
374
|
+
agent = sdk.aegis.get_by_client_id(client_id)
|
|
375
|
+
if not agent:
|
|
376
|
+
raise click.ClickException(f"Agent not found: {client_id}")
|
|
377
|
+
# CLIENT_ID names a legacy agent's host. A v2 endpoint has no host target on this
|
|
378
|
+
# path and is only ever reached through the --endpoint pin, so refuse here rather
|
|
379
|
+
# than hand the SDK a bare hostname that would lose the agent's v2 identity.
|
|
380
|
+
if is_v2_agent(agent):
|
|
381
|
+
endpoint = getattr(agent, 'endpoint_id', None) or '<endpoint id>'
|
|
382
|
+
raise click.UsageError(
|
|
383
|
+
f'{client_id} is an Aegis v2 endpoint, which the legacy job path cannot target. '
|
|
384
|
+
f'To run a mobile capability on it, pass --package <application id> -e {endpoint}.')
|
|
385
|
+
|
|
353
386
|
try:
|
|
354
387
|
result = sdk.aegis.run_job(
|
|
355
388
|
agent,
|
|
356
|
-
list(capabilities) if capabilities else None,
|
|
357
|
-
|
|
389
|
+
capabilities=list(capabilities) if capabilities else None,
|
|
390
|
+
package=package,
|
|
391
|
+
endpoint_id=endpoint_id,
|
|
392
|
+
config=config,
|
|
358
393
|
)
|
|
359
394
|
|
|
360
395
|
if 'capabilities' in result:
|
|
396
|
+
if not result['capabilities']:
|
|
397
|
+
click.echo('No capabilities available for this target.')
|
|
398
|
+
return
|
|
361
399
|
click.echo("Available capabilities:")
|
|
362
400
|
for cap in result['capabilities']:
|
|
363
401
|
name = cap.get('name', 'unknown')
|
|
@@ -366,6 +404,9 @@ def job(ctx, sdk, capabilities, config, client_id):
|
|
|
366
404
|
elif result.get('success'):
|
|
367
405
|
click.echo("✓ Job queued successfully")
|
|
368
406
|
click.echo(f" Job ID: {result.get('job_id', 'unknown')}")
|
|
407
|
+
click.echo(f" Target: {result.get('target_key', 'unknown')}")
|
|
408
|
+
if result.get('endpoint_id'):
|
|
409
|
+
click.echo(f" Endpoint: {result['endpoint_id']}")
|
|
369
410
|
click.echo(f" Status: {result.get('status', 'unknown')}")
|
|
370
411
|
else:
|
|
371
412
|
click.echo("Error: Unknown error", err=True)
|
|
@@ -67,10 +67,9 @@ def credential_active_directory(sdk, credential_id, endpoint_ids):
|
|
|
67
67
|
@click.argument('key', required=True)
|
|
68
68
|
@click.option('-s', '--status', type=click.Choice([s.value for s in Risk]), help=f'Status of the risk')
|
|
69
69
|
@click.option('-c', '--comment', default='', help='Comment for the risk')
|
|
70
|
-
@click.option('-r', '--remove-comment', type=int, default=None, help='Remove comment at index (0, 1, ... or -1 for most recent)')
|
|
71
70
|
@click.option('--title', '-t', default=None, help='Human-readable title for the risk')
|
|
72
71
|
@click.option('-g', '--tag', 'tags', multiple=True, help='Tag for the risk (can be specified multiple times)')
|
|
73
|
-
def risk(chariot, key, status, comment,
|
|
72
|
+
def risk(chariot, key, status, comment, title, tags):
|
|
74
73
|
""" Update the status and comment of a risk
|
|
75
74
|
|
|
76
75
|
\b
|
|
@@ -81,15 +80,10 @@ def risk(chariot, key, status, comment, remove_comment, title, tags):
|
|
|
81
80
|
Example usages:
|
|
82
81
|
- guard update risk "#risk#www.example.com#CVE-2024-23049" --status OH --comment "Open it as a high severity risk"
|
|
83
82
|
- guard update risk "#risk#www.example.com#open-ssh-port" --status RH --comment "John stopped sshd on the server"
|
|
84
|
-
- guard update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment 0
|
|
85
|
-
- guard update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment -1
|
|
86
83
|
- guard update risk "#risk#www.example.com#CVE-2024-23049" --tag resolved --tag verified
|
|
87
84
|
"""
|
|
88
|
-
if comment and remove_comment is not None:
|
|
89
|
-
raise click.UsageError("Cannot use --comment and --remove-comment together")
|
|
90
|
-
|
|
91
85
|
key = resolve_entity_reference(chariot, key, 'risk')
|
|
92
|
-
chariot.risks.update(key, status, comment,
|
|
86
|
+
chariot.risks.update(key, status, comment, title, tags)
|
|
93
87
|
|
|
94
88
|
|
|
95
89
|
@update.command()
|
|
@@ -5,6 +5,7 @@ from praetorian_cli.sdk.entities.ad import AD
|
|
|
5
5
|
from praetorian_cli.sdk.entities.aegis import Aegis
|
|
6
6
|
from praetorian_cli.sdk.entities.agents import Agents
|
|
7
7
|
from praetorian_cli.sdk.entities.assets import Assets
|
|
8
|
+
from praetorian_cli.sdk.entities.apks import Apks
|
|
8
9
|
from praetorian_cli.sdk.entities.attributes import Attributes
|
|
9
10
|
from praetorian_cli.sdk.entities.capabilities import Capabilities
|
|
10
11
|
from praetorian_cli.sdk.entities.configurations import Configurations
|
|
@@ -70,6 +71,7 @@ class Chariot:
|
|
|
70
71
|
self.schema = Schema(self)
|
|
71
72
|
self.schedules = Schedules(self)
|
|
72
73
|
self.vms = EngineerVms(self)
|
|
74
|
+
self.apks = Apks(self)
|
|
73
75
|
self.proxy = proxy
|
|
74
76
|
|
|
75
77
|
if self.proxy == '' and os.environ.get('CHARIOT_PROXY'):
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
|
-
|
|
2
|
+
import json
|
|
3
3
|
import logging
|
|
4
|
+
from urllib.parse import quote
|
|
4
5
|
import shlex
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import time
|
|
8
9
|
from praetorian_cli.sdk.model.aegis import Agent, is_v2_agent, validate_agent_for_ssh
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
def normalize_to_list(value, item_keys: List[str] = None) -> List:
|
|
12
12
|
keys = item_keys or ["items", "data", "capabilities", "assets"]
|
|
13
13
|
if value is None:
|
|
@@ -115,6 +115,10 @@ CLOUDFLARE_TUNNEL_STATUS_PATH = 'aegis/management/cloudflare/tunnel/status/{endp
|
|
|
115
115
|
CLOUDFLARE_TUNNEL_CREATE_PATH = 'aegis/management/cloudflare/tunnel/create'
|
|
116
116
|
CLOUDFLARE_TUNNEL_REMOVE_PATH = 'aegis/management/cloudflare/tunnel/remove'
|
|
117
117
|
ENDPOINT_PIN_CONFIG_KEY = 'endpoint_agent_id'
|
|
118
|
+
# The endpoint kind that runs Aegis capabilities, mobile ones included.
|
|
119
|
+
ENDPOINT_KIND_AEGIS = 'aegis'
|
|
120
|
+
# Connection state in which an endpoint can pick up work.
|
|
121
|
+
ENDPOINT_STATE_ONLINE = 'online'
|
|
118
122
|
LINUX_SYSTEM_ADDUSER = 'linux-system-adduser'
|
|
119
123
|
LINUX_SYSTEM_DELUSER = 'linux-system-deluser'
|
|
120
124
|
|
|
@@ -412,17 +416,29 @@ class Aegis:
|
|
|
412
416
|
),
|
|
413
417
|
)
|
|
414
418
|
|
|
415
|
-
def get_capabilities(self, surface_filter: str = None, agent_os: str = None
|
|
419
|
+
def get_capabilities(self, surface_filter: str = None, agent_os: str = None,
|
|
420
|
+
target: str = None, endpoint_kind: str = None,
|
|
421
|
+
executor: str = 'aegis') -> List[dict]:
|
|
416
422
|
"""
|
|
417
423
|
Get Aegis capabilities with optional filtering.
|
|
418
424
|
|
|
419
425
|
Retrieves available capabilities that can be executed by Aegis agents,
|
|
420
426
|
with optional filtering by attack surface and operating system.
|
|
421
427
|
|
|
422
|
-
:param surface_filter: Filter by attack surface type (e.g., 'internal', 'external')
|
|
428
|
+
:param surface_filter: Filter by attack surface type (e.g., 'internal', 'external', 'mobile')
|
|
423
429
|
:type surface_filter: str or None
|
|
424
430
|
:param agent_os: Filter by agent operating system (e.g., 'windows', 'linux')
|
|
425
431
|
:type agent_os: str or None
|
|
432
|
+
:param target: Filter by target type (e.g., 'asset', 'addomain', 'apk')
|
|
433
|
+
:type target: str or None
|
|
434
|
+
:param endpoint_kind: Filter to capabilities dispatchable to an enrolled endpoint of
|
|
435
|
+
this kind (e.g., 'aegis')
|
|
436
|
+
:type endpoint_kind: str or None
|
|
437
|
+
:param executor: Filter by executor. Defaults to 'aegis', which is the executor for
|
|
438
|
+
capabilities that run on an SSH-reachable Aegis agent. Endpoint-dispatched
|
|
439
|
+
capabilities -- including every mobile one -- are registered against Guard's own
|
|
440
|
+
executor instead, so pass '' to include them.
|
|
441
|
+
:type executor: str
|
|
426
442
|
:return: List of capability dictionaries
|
|
427
443
|
:rtype: list
|
|
428
444
|
|
|
@@ -436,6 +452,9 @@ class Aegis:
|
|
|
436
452
|
>>> # Get Windows capabilities for internal surface
|
|
437
453
|
>>> win_caps = sdk.aegis.get_capabilities(surface_filter='internal', agent_os='windows')
|
|
438
454
|
|
|
455
|
+
>>> # Get the mobile capabilities dispatchable to an enrolled Android endpoint
|
|
456
|
+
>>> mobile_caps = sdk.aegis.get_capabilities(target='apk', endpoint_kind='aegis', executor='')
|
|
457
|
+
|
|
439
458
|
**Capability Object Properties:**
|
|
440
459
|
Each capability contains:
|
|
441
460
|
- name: Capability name (e.g., 'windows-smb-snaffler')
|
|
@@ -445,7 +464,8 @@ class Aegis:
|
|
|
445
464
|
- parameters: List of configurable parameters
|
|
446
465
|
"""
|
|
447
466
|
try:
|
|
448
|
-
capabilities_response = self.api.capabilities.list(
|
|
467
|
+
capabilities_response = self.api.capabilities.list(
|
|
468
|
+
target=target or '', executor=executor or '', endpoint_kind=endpoint_kind or '')
|
|
449
469
|
|
|
450
470
|
# Handle different response formats
|
|
451
471
|
if isinstance(capabilities_response, tuple):
|
|
@@ -829,30 +849,88 @@ class Aegis:
|
|
|
829
849
|
else:
|
|
830
850
|
print(f"\033[31m✗ Transfer failed (exit code {returncode})\033[0m")
|
|
831
851
|
|
|
832
|
-
def run_job(self, agent: Agent, capabilities: list = None, config: str = None
|
|
852
|
+
def run_job(self, agent: Agent = None, capabilities: list = None, config: str = None,
|
|
853
|
+
hostname: str = None, package: str = None, endpoint_id: str = None) -> dict:
|
|
833
854
|
"""
|
|
834
|
-
Run a job on an Aegis agent.
|
|
855
|
+
Run a job on an Aegis agent or on an enrolled endpoint.
|
|
856
|
+
|
|
857
|
+
Name the target with a hostname, for an agent's own host asset, or with a package,
|
|
858
|
+
for an Android application already registered by sdk.apks.add(). A package targets
|
|
859
|
+
'#apk#<package>' -- what every mobile capability matches against, and the asset its
|
|
860
|
+
findings are filed on.
|
|
861
|
+
|
|
862
|
+
An Aegis v2 endpoint runs the job only when endpoint_id names it. The endpoint is
|
|
863
|
+
not taken from a v2 agent automatically: dispatching to a device is a decision the
|
|
864
|
+
caller makes rather than a consequence of which row they had selected.
|
|
865
|
+
|
|
866
|
+
Apart from the legacy agent argument, every parameter is a string or a list, so
|
|
867
|
+
this method is callable over the MCP server as well as from Python. There is no
|
|
868
|
+
second code path for MCP.
|
|
869
|
+
|
|
870
|
+
:param agent: Agent object from sdk.aegis.list(), naming a v1 agent's host as the
|
|
871
|
+
target. Prefer hostname; this exists so existing positional callers keep working.
|
|
872
|
+
:type agent: Agent or None
|
|
873
|
+
:param capabilities: Capability names to run. When omitted, no job is created and
|
|
874
|
+
the capabilities available for the named target are returned instead.
|
|
875
|
+
:type capabilities: list or None
|
|
876
|
+
:param config: JSON object string of capability parameters, e.g.
|
|
877
|
+
'{"command": "getprop ro.build.fingerprint"}'
|
|
878
|
+
:type config: str or None
|
|
879
|
+
:param hostname: Hostname of an Aegis agent, to target that agent's host asset
|
|
880
|
+
:type hostname: str or None
|
|
881
|
+
:param package: Android application ID, e.g. 'com.bank.app', to target '#apk#<package>'
|
|
882
|
+
:type package: str or None
|
|
883
|
+
:param endpoint_id: Enrolled Aegis v2 endpoint to run the job on, as shown by
|
|
884
|
+
"guard aegis list --details". Passed to Guard as the job's endpoint_agent_id.
|
|
885
|
+
Guard reserves that key for Praetorian users and silently drops it for everyone
|
|
886
|
+
else, in which case the job falls back to automatic endpoint selection.
|
|
887
|
+
:type endpoint_id: str or None
|
|
888
|
+
:return: When capabilities are given, a dict with 'success', 'job_id', 'job_key',
|
|
889
|
+
'status', 'target_key' and 'endpoint_id'. Otherwise a dict with 'capabilities'.
|
|
890
|
+
:rtype: dict
|
|
891
|
+
:raises Exception: If no target is named, if two are, if a v2 agent is named without
|
|
892
|
+
an endpoint_id, or if the named endpoint is not enrolled, connected and accepting work
|
|
893
|
+
|
|
894
|
+
**Example Usage:**
|
|
895
|
+
>>> # What can run against an APK on an enrolled endpoint
|
|
896
|
+
>>> sdk.aegis.run_job(package='com.bank.app')
|
|
835
897
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
898
|
+
>>> # Dispatch a mobile capability to a named device
|
|
899
|
+
>>> sdk.aegis.run_job(capabilities=['android-device-survey'],
|
|
900
|
+
>>> package='com.bank.app',
|
|
901
|
+
>>> endpoint_id='16169bc5-7943-4783-af81-c4735616f7e9')
|
|
902
|
+
|
|
903
|
+
>>> # Dispatch a host capability to an Aegis agent, as before
|
|
904
|
+
>>> sdk.aegis.run_job(capabilities=['linux-enum'], hostname='agent01')
|
|
839
905
|
"""
|
|
840
|
-
|
|
906
|
+
# A v2 endpoint is reached only through the pin. Without one this is the legacy
|
|
907
|
+
# path, which has no way to route to a device, so it refuses rather than quietly
|
|
908
|
+
# running the job somewhere else.
|
|
909
|
+
if is_v2_agent(agent) and not endpoint_id:
|
|
841
910
|
raise Exception(
|
|
842
911
|
"Aegis v2 endpoint job execution is not supported by the legacy "
|
|
843
912
|
"Aegis job path; refusing to run without endpoint_agent_id"
|
|
844
913
|
)
|
|
845
914
|
|
|
915
|
+
if agent is not None and not hostname and not package and not is_v2_agent(agent):
|
|
916
|
+
hostname = agent.hostname or 'unknown'
|
|
917
|
+
|
|
918
|
+
if hostname and package:
|
|
919
|
+
raise Exception('A job has one target: pass either hostname or package, not both.')
|
|
920
|
+
|
|
846
921
|
if not capabilities:
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
922
|
+
return {'capabilities': sorted(self._offerable_capabilities(package, endpoint_id),
|
|
923
|
+
key=lambda capability: capability.get('name', ''))}
|
|
924
|
+
|
|
925
|
+
target_key = self._target_key(hostname, package)
|
|
851
926
|
|
|
852
|
-
|
|
853
|
-
|
|
927
|
+
# Checked before the job is created, not after. Guard queues a job for an endpoint
|
|
928
|
+
# that cannot run it just as readily as for one that can, and the job then sits at
|
|
929
|
+
# its initial status with nothing saying why -- which reads as a silent success.
|
|
930
|
+
if endpoint_id:
|
|
931
|
+
self._verify_endpoint_can_accept_work(endpoint_id)
|
|
854
932
|
|
|
855
|
-
jobs = self.api.jobs.add(target_key, list(capabilities), config)
|
|
933
|
+
jobs = self.api.jobs.add(target_key, list(capabilities), self._job_config(config, endpoint_id))
|
|
856
934
|
if not jobs:
|
|
857
935
|
raise Exception("No job returned from API")
|
|
858
936
|
|
|
@@ -865,8 +943,75 @@ class Aegis:
|
|
|
865
943
|
'job_id': job_key.split('#')[-1][:12] if job_key else 'unknown',
|
|
866
944
|
'job_key': job_key,
|
|
867
945
|
'status': status,
|
|
946
|
+
'target_key': target_key,
|
|
947
|
+
'endpoint_id': endpoint_id or '',
|
|
868
948
|
}
|
|
869
|
-
|
|
949
|
+
|
|
950
|
+
def _offerable_capabilities(self, package: str = None, endpoint_id: str = None) -> List[dict]:
|
|
951
|
+
"""Capabilities worth offering for the target the caller has named.
|
|
952
|
+
|
|
953
|
+
An APK target gets the capabilities registered against APKs and dispatchable to an
|
|
954
|
+
enrolled endpoint, so a host capability is never offered for an application. With
|
|
955
|
+
no target named, the host capabilities are listed as before.
|
|
956
|
+
"""
|
|
957
|
+
if package:
|
|
958
|
+
return self.get_capabilities(target='apk', endpoint_kind=ENDPOINT_KIND_AEGIS, executor='')
|
|
959
|
+
if endpoint_id:
|
|
960
|
+
return self.get_capabilities(endpoint_kind=ENDPOINT_KIND_AEGIS, executor='')
|
|
961
|
+
return self.get_capabilities(surface_filter='internal')
|
|
962
|
+
|
|
963
|
+
@staticmethod
|
|
964
|
+
def _target_key(hostname: str = None, package: str = None) -> str:
|
|
965
|
+
if package:
|
|
966
|
+
return f"#apk#{package}"
|
|
967
|
+
if hostname:
|
|
968
|
+
return f"#asset#{hostname}#{hostname}"
|
|
969
|
+
raise Exception('No target for the job. Pass hostname to target an Aegis agent, or '
|
|
970
|
+
'package to target an APK, e.g. package="com.bank.app".')
|
|
971
|
+
|
|
972
|
+
def _endpoint_row(self, endpoint_id: str) -> Optional[dict]:
|
|
973
|
+
"""The durable inventory row for one endpoint, or None if it is not enrolled."""
|
|
974
|
+
for row in self._list_endpoint_inventory_rows():
|
|
975
|
+
if _endpoint_row_id(row) == endpoint_id:
|
|
976
|
+
return row
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
def _verify_endpoint_can_accept_work(self, endpoint_id: str) -> dict:
|
|
980
|
+
endpoint = self._endpoint_row(endpoint_id)
|
|
981
|
+
if endpoint is None:
|
|
982
|
+
raise Exception(f'Endpoint "{endpoint_id}" is not enrolled. '
|
|
983
|
+
f'Run "guard aegis list --details" to see the enrolled endpoints.')
|
|
984
|
+
|
|
985
|
+
state = endpoint.get('connectionState') or 'unknown'
|
|
986
|
+
if state != ENDPOINT_STATE_ONLINE:
|
|
987
|
+
raise Exception(f'Endpoint "{endpoint_id}" is {state}, so it cannot pick up this job. '
|
|
988
|
+
f'Bring the device back online and try again.')
|
|
989
|
+
|
|
990
|
+
if endpoint.get('taskDispatchPaused'):
|
|
991
|
+
raise Exception(f'Endpoint "{endpoint_id}" is online but has task dispatch paused, '
|
|
992
|
+
f'so it would not pick up this job.')
|
|
993
|
+
|
|
994
|
+
return endpoint
|
|
995
|
+
|
|
996
|
+
@staticmethod
|
|
997
|
+
def _job_config(config: str = None, endpoint_id: str = None) -> Optional[str]:
|
|
998
|
+
"""Merge the endpoint pin into the caller's config, without losing what they set."""
|
|
999
|
+
merged = {}
|
|
1000
|
+
if isinstance(config, dict):
|
|
1001
|
+
merged = dict(config)
|
|
1002
|
+
elif config and config.strip():
|
|
1003
|
+
try:
|
|
1004
|
+
merged = json.loads(config)
|
|
1005
|
+
except json.JSONDecodeError as e:
|
|
1006
|
+
raise Exception(f'config is not valid JSON: {e}')
|
|
1007
|
+
if not isinstance(merged, dict):
|
|
1008
|
+
raise Exception('config must be a JSON object, e.g. \'{"command": "id"}\'')
|
|
1009
|
+
|
|
1010
|
+
if endpoint_id:
|
|
1011
|
+
merged[ENDPOINT_PIN_CONFIG_KEY] = endpoint_id
|
|
1012
|
+
|
|
1013
|
+
return json.dumps(merged) if merged else None
|
|
1014
|
+
|
|
870
1015
|
def format_agents_list(self, details: bool = False, filter_text: str = None):
|
|
871
1016
|
"""
|
|
872
1017
|
Format agents list for display with optional filtering and details.
|
|
@@ -928,3 +1073,4 @@ class Aegis:
|
|
|
928
1073
|
for i, agent in enumerate(agents_data, 1):
|
|
929
1074
|
lines.append(f"{i:>4} {agent.version:<7} {(agent.hostname or 'Unknown')[:30]:<30} {agent.display_id}")
|
|
930
1075
|
return warning_text + '\n'.join(lines)
|
|
1076
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Apks:
|
|
5
|
+
""" The methods in this class are to be accessed from sdk.apks, where sdk is an instance
|
|
6
|
+
of Chariot. """
|
|
7
|
+
|
|
8
|
+
def __init__(self, api):
|
|
9
|
+
self.api = api
|
|
10
|
+
|
|
11
|
+
def add(self, local_filepath: str, chariot_filepath: str = None) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Upload an Android APK and register it as an asset.
|
|
14
|
+
|
|
15
|
+
This is two calls, because that is what Guard expects: the bytes go straight to
|
|
16
|
+
storage with a presigned PUT, then POST /apk registers the stored object as an
|
|
17
|
+
asset. No request carries the binary through the API.
|
|
18
|
+
|
|
19
|
+
The package name is not a parameter. It is read server-side from the APK's
|
|
20
|
+
AndroidManifest.xml, so the asset key is always '#apk#<package>' as declared by
|
|
21
|
+
the binary itself, and a caller cannot register an arbitrary identity for an
|
|
22
|
+
arbitrary file. Re-uploading a new build of the same app updates the existing
|
|
23
|
+
asset rather than forking a second one.
|
|
24
|
+
|
|
25
|
+
:param local_filepath: Path to the local .apk file to upload
|
|
26
|
+
:type local_filepath: str
|
|
27
|
+
:param chariot_filepath: Destination path in Guard storage. Defaults to the
|
|
28
|
+
local file's base name.
|
|
29
|
+
:type chariot_filepath: str or None
|
|
30
|
+
:return: The registered APK asset, including its key, package and version
|
|
31
|
+
:rtype: dict
|
|
32
|
+
:raises Exception: If the file is missing, is not named .apk, or does not hold a
|
|
33
|
+
readable Android manifest
|
|
34
|
+
|
|
35
|
+
**Example Usage:**
|
|
36
|
+
>>> apk = sdk.apks.add('./bank-app.apk')
|
|
37
|
+
>>> print(apk['key'])
|
|
38
|
+
#apk#com.bank.app
|
|
39
|
+
"""
|
|
40
|
+
if not os.path.isfile(local_filepath):
|
|
41
|
+
raise Exception(f'APK not found: {local_filepath}')
|
|
42
|
+
|
|
43
|
+
if not chariot_filepath:
|
|
44
|
+
chariot_filepath = os.path.basename(local_filepath)
|
|
45
|
+
|
|
46
|
+
# POST /apk rejects an artifact_key that does not end in .apk, but only after
|
|
47
|
+
# the upload has already happened. Checking here means a mis-named file costs
|
|
48
|
+
# nothing instead of costing the whole transfer.
|
|
49
|
+
if not chariot_filepath.lower().endswith('.apk'):
|
|
50
|
+
raise Exception(f'the storage path must end in .apk, got "{chariot_filepath}"')
|
|
51
|
+
|
|
52
|
+
self.api.upload(local_filepath, chariot_filepath)
|
|
53
|
+
return self.api.post('apk', dict(artifact_key=chariot_filepath))
|