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.
Files changed (229) hide show
  1. {praetorian_cli-2.6.2/praetorian_cli.egg-info → praetorian_cli-2.6.3}/PKG-INFO +15 -2
  2. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/README.md +12 -0
  3. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/add.py +28 -3
  4. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/aegis.py +52 -11
  5. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/update.py +2 -8
  6. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/chariot.py +2 -0
  7. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/aegis.py +165 -19
  8. praetorian_cli-2.6.3/praetorian_cli/sdk/entities/apks.py +53 -0
  9. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/risks.py +12 -57
  10. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/mcp_server.py +101 -52
  11. praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_aegis_job_cli.py +120 -0
  12. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_agent.py +1 -1
  13. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_bulk.py +1 -4
  14. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_mcp.py +65 -0
  15. praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_mcp_mobile_tools.py +105 -0
  16. praetorian_cli-2.6.3/praetorian_cli/sdk/test/test_mobile_dispatch.py +288 -0
  17. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_risk.py +31 -29
  18. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_search.py +1 -1
  19. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/utils.py +0 -1
  20. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/data.py +1 -2
  21. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3/praetorian_cli.egg-info}/PKG-INFO +15 -2
  22. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/SOURCES.txt +4 -0
  23. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/requires.txt +2 -1
  24. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/setup.cfg +3 -2
  25. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/LICENSE +0 -0
  26. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/MANIFEST.in +0 -0
  27. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/__init__.py +0 -0
  28. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/__init__.py +0 -0
  29. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/access.py +0 -0
  30. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/agent.py +0 -0
  31. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/chariot.py +0 -0
  32. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/cli_decorators.py +0 -0
  33. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/configure.py +0 -0
  34. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/critfinder.py +0 -0
  35. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/delete.py +0 -0
  36. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/engagement.py +0 -0
  37. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/enrich.py +0 -0
  38. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/export.py +0 -0
  39. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/find.py +0 -0
  40. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/get.py +0 -0
  41. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/hunt.py +0 -0
  42. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/imports.py +0 -0
  43. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/link.py +0 -0
  44. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/list.py +0 -0
  45. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/query.py +0 -0
  46. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/report.py +0 -0
  47. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/run.py +0 -0
  48. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/script.py +0 -0
  49. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/search.py +0 -0
  50. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/ssh_utils.py +0 -0
  51. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/test.py +0 -0
  52. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/unlink.py +0 -0
  53. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/utils.py +0 -0
  54. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/vm.py +0 -0
  55. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/handlers/vm_proxy.py +0 -0
  56. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/main.py +0 -0
  57. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/runners/__init__.py +0 -0
  58. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/runners/local.py +0 -0
  59. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/__init__.py +0 -0
  60. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/commands/__init__.py +0 -0
  61. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  62. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/scripts/utils.py +0 -0
  63. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/__init__.py +0 -0
  64. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/__init__.py +0 -0
  65. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/account_discovery.py +0 -0
  66. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/accounts.py +0 -0
  67. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/ad.py +0 -0
  68. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/agents.py +0 -0
  69. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/assets.py +0 -0
  70. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/attributes.py +0 -0
  71. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/capabilities.py +0 -0
  72. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/configurations.py +0 -0
  73. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/conversations.py +0 -0
  74. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/credentials.py +0 -0
  75. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/definitions.py +0 -0
  76. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/endpoint_executions.py +0 -0
  77. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/engineer_vm.py +0 -0
  78. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/files.py +0 -0
  79. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/hunts.py +0 -0
  80. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/integrations.py +0 -0
  81. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/jobs.py +0 -0
  82. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/keys.py +0 -0
  83. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/preseeds.py +0 -0
  84. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/reports.py +0 -0
  85. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/scanners.py +0 -0
  86. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/schedules.py +0 -0
  87. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/schema.py +0 -0
  88. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/search.py +0 -0
  89. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/seeds.py +0 -0
  90. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/settings.py +0 -0
  91. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/statistics.py +0 -0
  92. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/webhook.py +0 -0
  93. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/entities/webpage.py +0 -0
  94. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/exceptions.py +0 -0
  95. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/guard.py +0 -0
  96. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/keychain.py +0 -0
  97. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/__init__.py +0 -0
  98. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/aegis.py +0 -0
  99. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/globals.py +0 -0
  100. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/query.py +0 -0
  101. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/utils.py +0 -0
  102. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/model/vm.py +0 -0
  103. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/__init__.py +0 -0
  104. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/pytest.ini +0 -0
  105. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_access_aws.py +0 -0
  106. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_access_github.py +0 -0
  107. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_account.py +0 -0
  108. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_add_file_cli.py +0 -0
  109. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_endpoint_live.py +0 -0
  110. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_management_users.py +0 -0
  111. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_network_policy.py +0 -0
  112. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_tunnel.py +0 -0
  113. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_aegis_v2_enrollment.py +0 -0
  114. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_agent_interactions.py +0 -0
  115. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_asset.py +0 -0
  116. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_async_http.py +0 -0
  117. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  118. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  119. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_cli_errors.py +0 -0
  120. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  121. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation.py +0 -0
  122. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation_approval_surfaces.py +0 -0
  123. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_conversation_interactions.py +0 -0
  124. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_credential_process.py +0 -0
  125. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_credentials.py +0 -0
  126. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_definition.py +0 -0
  127. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_approvals.py +0 -0
  128. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_execution_cli.py +0 -0
  129. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_executions.py +0 -0
  130. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_network_policy.py +0 -0
  131. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_endpoint_status.py +0 -0
  132. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_engineer_vm.py +0 -0
  133. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_entity_resolution_cli.py +0 -0
  134. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_entity_resolver.py +0 -0
  135. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_export_cli.py +0 -0
  136. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_extend.py +0 -0
  137. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_file.py +0 -0
  138. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_file_verified_download.py +0 -0
  139. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_all_surface_cli.py +0 -0
  140. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_chat.py +0 -0
  141. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_chat_live.py +0 -0
  142. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_data.py +0 -0
  143. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_internal_cli.py +0 -0
  144. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_launch.py +0 -0
  145. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_memory_browser.py +0 -0
  146. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_open.py +0 -0
  147. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_overview.py +0 -0
  148. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_scope_selector.py +0 -0
  149. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunt_workflows.py +0 -0
  150. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_hunts_internal.py +0 -0
  151. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_job.py +0 -0
  152. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_key.py +0 -0
  153. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_keychain.py +0 -0
  154. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_keychain_cache.py +0 -0
  155. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_local_runner.py +0 -0
  156. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_my_by_raw_query.py +0 -0
  157. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  158. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_report.py +0 -0
  159. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_run_cli.py +0 -0
  160. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_sdk_exceptions.py +0 -0
  161. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_seed.py +0 -0
  162. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_setting.py +0 -0
  163. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_terminal.py +0 -0
  164. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_test_handler.py +0 -0
  165. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_test_target.py +0 -0
  166. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_update_check.py +0 -0
  167. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  168. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_webpage.py +0 -0
  169. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/test_z_cli.py +0 -0
  170. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
  171. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/__init__.py +0 -0
  172. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/__init__.py +0 -0
  173. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/account_selector.py +0 -0
  174. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
  175. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
  176. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/enrollment.py +0 -0
  177. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/help.py +0 -0
  178. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/hunt.py +0 -0
  179. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/info.py +0 -0
  180. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job.py +0 -0
  181. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
  182. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/job_v2.py +0 -0
  183. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/list.py +0 -0
  184. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/network_policy.py +0 -0
  185. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
  186. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
  187. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
  188. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/set.py +0 -0
  189. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
  190. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/tunnel.py +0 -0
  191. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/commands/user.py +0 -0
  192. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/constants.py +0 -0
  193. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/menu.py +0 -0
  194. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/theme.py +0 -0
  195. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/aegis/utils.py +0 -0
  196. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/async_http.py +0 -0
  197. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/__init__.py +0 -0
  198. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/__init__.py +0 -0
  199. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/accounts.py +0 -0
  200. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/context.py +0 -0
  201. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/marcus.py +0 -0
  202. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/reporting.py +0 -0
  203. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/search.py +0 -0
  204. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/commands/tools.py +0 -0
  205. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/console.py +0 -0
  206. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/context.py +0 -0
  207. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/console/renderer.py +0 -0
  208. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/__init__.py +0 -0
  209. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/approvals.py +0 -0
  210. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/endpoint_status.py +0 -0
  211. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
  212. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/entity_resolver.py +0 -0
  213. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/entity_selector.py +0 -0
  214. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_chat.py +0 -0
  215. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_chat_live.py +0 -0
  216. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_data.py +0 -0
  217. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_defaults.py +0 -0
  218. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_format.py +0 -0
  219. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_launch.py +0 -0
  220. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_memory.py +0 -0
  221. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_open.py +0 -0
  222. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_overview.py +0 -0
  223. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/hunt_workflows.py +0 -0
  224. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/terminal.py +0 -0
  225. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli/ui/textual_refresh.py +0 -0
  226. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  227. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/entry_points.txt +0 -0
  228. {praetorian_cli-2.6.2 → praetorian_cli-2.6.3}/praetorian_cli.egg-info/top_level.txt +0 -0
  229. {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.2
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>=1.12.2
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, comment, capability, title, tags):
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, comment, capability, title, tags)
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, help='Capability to run (e.g., windows-smb-snaffler)')
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.argument('client_id', required=True)
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
- agent = sdk.aegis.get_by_client_id(client_id)
349
- if not agent:
350
- click.echo(f"Agent not found: {client_id}", err=True)
351
- return
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
- config
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, remove_comment, title, tags):
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, remove_comment, title, tags)
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
- from urllib.parse import quote
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) -> List[dict]:
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(executor='aegis')
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
- If no capabilities are provided, returns available capability dicts under
837
- the 'capabilities' key. When capabilities are provided, returns a dict
838
- with keys: 'success', 'job_id', 'job_key', 'status'. Errors raise.
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
- if is_v2_agent(agent):
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
- caps = self.get_capabilities(surface_filter='internal')
848
- return {
849
- 'capabilities': sorted(caps, key=lambda x: x.get('name', '')),
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
- hostname = agent.hostname or 'unknown'
853
- target_key = f"#asset#{hostname}#{hostname}"
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))