argus-cloud-optimizer 0.6.1__tar.gz → 0.7.0__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 (183) hide show
  1. {argus_cloud_optimizer-0.6.1/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.7.0}/PKG-INFO +62 -17
  2. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/README.md +60 -14
  3. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/adapter.py +51 -2
  4. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/cloudtrail.py +5 -2
  5. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/cloudwatch.py +99 -49
  6. argus_cloud_optimizer-0.7.0/adapters/aws/cost_explorer.py +154 -0
  7. argus_cloud_optimizer-0.7.0/adapters/aws/logs_activity.py +61 -0
  8. argus_cloud_optimizer-0.7.0/adapters/aws/preflight.py +324 -0
  9. argus_cloud_optimizer-0.7.0/adapters/aws/resource_explorer.py +348 -0
  10. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/retry.py +4 -3
  11. argus_cloud_optimizer-0.7.0/adapters/aws/state.py +328 -0
  12. argus_cloud_optimizer-0.7.0/adapters/aws/tags.py +56 -0
  13. argus_cloud_optimizer-0.7.0/adapters/aws/type_map.py +150 -0
  14. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/monitor.py +2 -0
  15. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/base.py +35 -1
  16. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/cloud_monitoring.py +5 -0
  17. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/anthropic.py +40 -6
  18. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/azure_openai.py +9 -3
  19. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/bedrock.py +42 -11
  20. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/vertexai.py +9 -3
  21. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0/argus_cloud_optimizer.egg-info}/PKG-INFO +62 -17
  22. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/argus_cloud_optimizer.egg-info/SOURCES.txt +47 -2
  23. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/argus_cloud_optimizer.egg-info/requires.txt +1 -2
  24. argus_cloud_optimizer-0.7.0/core/__version__.py +1 -0
  25. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/agent/chat.py +7 -3
  26. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/agent/loop.py +336 -48
  27. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/agent/prompts.py +195 -15
  28. argus_cloud_optimizer-0.7.0/core/agent/selection.py +92 -0
  29. argus_cloud_optimizer-0.7.0/core/baseline/__init__.py +3 -0
  30. argus_cloud_optimizer-0.7.0/core/baseline/engine.py +217 -0
  31. argus_cloud_optimizer-0.7.0/core/baseline/rules.py +213 -0
  32. argus_cloud_optimizer-0.7.0/core/calibration/__init__.py +1 -0
  33. argus_cloud_optimizer-0.7.0/core/calibration/__main__.py +55 -0
  34. argus_cloud_optimizer-0.7.0/core/calibration/manifest.py +69 -0
  35. argus_cloud_optimizer-0.7.0/core/calibration/scoring.py +190 -0
  36. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/config.py +47 -3
  37. argus_cloud_optimizer-0.7.0/core/errors.py +25 -0
  38. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/log.py +17 -1
  39. argus_cloud_optimizer-0.7.0/core/models/coverage.py +157 -0
  40. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/models/finding.py +15 -0
  41. argus_cloud_optimizer-0.7.0/core/models/preflight.py +56 -0
  42. argus_cloud_optimizer-0.7.0/core/opportunities/__init__.py +9 -0
  43. argus_cloud_optimizer-0.7.0/core/opportunities/__main__.py +57 -0
  44. argus_cloud_optimizer-0.7.0/core/opportunities/actions.py +59 -0
  45. argus_cloud_optimizer-0.7.0/core/opportunities/grouping.py +298 -0
  46. argus_cloud_optimizer-0.7.0/core/opportunities/guidance.py +166 -0
  47. argus_cloud_optimizer-0.7.0/core/pipeline/__init__.py +1 -0
  48. argus_cloud_optimizer-0.7.0/core/pipeline/evidence.py +182 -0
  49. argus_cloud_optimizer-0.7.0/core/pipeline/gate.py +45 -0
  50. argus_cloud_optimizer-0.7.0/core/pipeline/guards.py +232 -0
  51. argus_cloud_optimizer-0.7.0/core/pipeline/runner.py +393 -0
  52. argus_cloud_optimizer-0.7.0/core/pipeline/triage.py +461 -0
  53. argus_cloud_optimizer-0.7.0/core/pricing/aws.py +75 -0
  54. argus_cloud_optimizer-0.7.0/core/progress.py +349 -0
  55. argus_cloud_optimizer-0.7.0/core/reports/basis.py +80 -0
  56. argus_cloud_optimizer-0.7.0/core/reports/board.py +719 -0
  57. argus_cloud_optimizer-0.7.0/core/reports/board_assets.py +472 -0
  58. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/reports/delivery.py +4 -7
  59. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/reports/export.py +14 -22
  60. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/reports/generator.py +113 -3
  61. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/reports/multi_cloud.py +44 -1
  62. argus_cloud_optimizer-0.7.0/core/reports/redact.py +148 -0
  63. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/deploy/aws/multi-account/spoke-role.yaml +13 -0
  64. argus_cloud_optimizer-0.7.0/deploy/aws/read-only-scan-role.yaml +94 -0
  65. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/deploy/aws/single-account/template.yaml +22 -0
  66. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/concepts/ai-providers.md +2 -0
  67. argus_cloud_optimizer-0.7.0/docs/concepts/baseline-rules.md +33 -0
  68. argus_cloud_optimizer-0.7.0/docs/concepts/board-report.md +110 -0
  69. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/concepts/how-it-works.md +29 -7
  70. argus_cloud_optimizer-0.7.0/docs/concepts/opportunities.md +81 -0
  71. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/contributing/index.md +1 -0
  72. argus_cloud_optimizer-0.7.0/docs/contributing/releasing.md +108 -0
  73. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/deployment/aws.md +1 -1
  74. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/getting-started/configuration.md +3 -3
  75. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/getting-started/first-scan.md +17 -31
  76. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/getting-started/quickstart.md +4 -4
  77. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/index.md +6 -9
  78. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/env-vars.md +18 -5
  79. argus_cloud_optimizer-0.7.0/docs/reference/exit-codes.md +16 -0
  80. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/iam-permissions.md +54 -0
  81. argus_cloud_optimizer-0.7.0/docs/reference/report-schema.md +131 -0
  82. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/troubleshooting.md +70 -0
  83. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/roadmap.md +14 -5
  84. argus_cloud_optimizer-0.7.0/docs/superpowers/plans/2026-10-22-trust-release.md +254 -0
  85. argus_cloud_optimizer-0.7.0/docs/superpowers/plans/2026-10-23-reliability-plan.md +48 -0
  86. argus_cloud_optimizer-0.7.0/entrypoints/_meta.py +108 -0
  87. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/aws_lambda.py +97 -6
  88. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/azure_function.py +33 -5
  89. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/cli.py +335 -28
  90. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/gcp_cloudrun.py +56 -6
  91. argus_cloud_optimizer-0.7.0/integrations/jira/__init__.py +0 -0
  92. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/pyproject.toml +3 -3
  93. argus_cloud_optimizer-0.7.0/tests/test_docs_consistency.py +253 -0
  94. argus_cloud_optimizer-0.7.0/tests/test_sample_board.py +44 -0
  95. argus_cloud_optimizer-0.7.0/tests/test_testbed.py +104 -0
  96. argus_cloud_optimizer-0.6.1/adapters/aws/cost_explorer.py +0 -116
  97. argus_cloud_optimizer-0.6.1/adapters/aws/resource_explorer.py +0 -186
  98. argus_cloud_optimizer-0.6.1/core/__version__.py +0 -1
  99. argus_cloud_optimizer-0.6.1/core/reports/html.py +0 -343
  100. argus_cloud_optimizer-0.6.1/docs/reference/report-schema.md +0 -72
  101. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/.env.example +0 -0
  102. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/LICENSE +0 -0
  103. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/MANIFEST.in +0 -0
  104. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/accounts.yaml.example +0 -0
  105. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/__init__.py +0 -0
  106. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/__init__.py +0 -0
  107. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/auth.py +0 -0
  108. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/aws/config.py +0 -0
  109. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/__init__.py +0 -0
  110. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/activity_log.py +0 -0
  111. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/adapter.py +0 -0
  112. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/cost_management.py +0 -0
  113. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/resource_graph.py +0 -0
  114. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/azure/retry.py +0 -0
  115. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/__init__.py +0 -0
  116. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/adapter.py +0 -0
  117. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/asset_inventory.py +0 -0
  118. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/billing.py +0 -0
  119. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/cloud_logging.py +0 -0
  120. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/adapters/gcp/retry.py +0 -0
  121. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/__init__.py +0 -0
  122. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/ai/base.py +0 -0
  123. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/argus_cloud_optimizer.egg-info/dependency_links.txt +0 -0
  124. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/argus_cloud_optimizer.egg-info/entry_points.txt +0 -0
  125. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/argus_cloud_optimizer.egg-info/top_level.txt +0 -0
  126. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/__init__.py +0 -0
  127. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/agent/__init__.py +0 -0
  128. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/models/__init__.py +0 -0
  129. {argus_cloud_optimizer-0.6.1/core/reports → argus_cloud_optimizer-0.7.0/core/pricing}/__init__.py +0 -0
  130. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/py.typed +0 -0
  131. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/__init__.py +0 -0
  132. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/aws.py +0 -0
  133. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/azure.py +0 -0
  134. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/factory.py +0 -0
  135. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/gcp.py +0 -0
  136. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/models.py +0 -0
  137. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/registry/registry.py +0 -0
  138. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/__init__.py +0 -0
  139. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/audit.py +0 -0
  140. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/engine.py +0 -0
  141. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/loader.py +0 -0
  142. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/models.py +0 -0
  143. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/rightsizing.py +0 -0
  144. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/remediation/validator.py +0 -0
  145. {argus_cloud_optimizer-0.6.1/entrypoints → argus_cloud_optimizer-0.7.0/core/reports}/__init__.py +0 -0
  146. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/reports/comparison.py +0 -0
  147. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/secrets.py +0 -0
  148. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/token_tracker.py +0 -0
  149. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/core/validation.py +0 -0
  150. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/deploy/aws/multi-account/hub/template.yaml +0 -0
  151. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/deploy/azure/function-app.bicep +0 -0
  152. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/deploy/gcp/deploy.sh +0 -0
  153. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/blog/index.md +0 -0
  154. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/blog/posts/react-vs-rule-based.md +0 -0
  155. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/concepts/adapters.md +0 -0
  156. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/concepts/ai-reasoning.md +0 -0
  157. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/concepts/index.md +0 -0
  158. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/contributing/development.md +0 -0
  159. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/contributing/new-adapter.md +0 -0
  160. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/contributing/new-ai-provider.md +0 -0
  161. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/deployment/azure.md +0 -0
  162. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/deployment/gcp.md +0 -0
  163. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/deployment/index.md +0 -0
  164. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/deployment/multi-account.md +0 -0
  165. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/getting-started/index.md +0 -0
  166. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/index.md +0 -0
  167. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/resource-types.md +0 -0
  168. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/reference/security.md +0 -0
  169. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/remediation/cli.md +0 -0
  170. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/remediation/index.md +0 -0
  171. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/remediation/jira.md +0 -0
  172. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/docs/remediation/policies.md +0 -0
  173. {argus_cloud_optimizer-0.6.1/integrations → argus_cloud_optimizer-0.7.0/entrypoints}/__init__.py +0 -0
  174. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/_remediation.py +0 -0
  175. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/entrypoints/cli_chat.py +0 -0
  176. {argus_cloud_optimizer-0.6.1/integrations/jira → argus_cloud_optimizer-0.7.0/integrations}/__init__.py +0 -0
  177. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/integrations/base.py +0 -0
  178. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/integrations/jira/client.py +0 -0
  179. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/integrations/jira/formatter.py +0 -0
  180. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/integrations/jira/tracker.py +0 -0
  181. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/integrations/jira/webhook.py +0 -0
  182. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/setup.cfg +0 -0
  183. {argus_cloud_optimizer-0.6.1 → argus_cloud_optimizer-0.7.0}/tests/test_packaging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-cloud-optimizer
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: Argus — AI Cloud Detective for AWS, GCP, and Azure
5
5
  Author-email: Vamshi Siddarth Gaddam <vamshisiddarth02@gmail.com>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Classifier: Topic :: System :: Systems Administration
19
19
  Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: anthropic>=0.40.0
22
+ Requires-Dist: anthropic<3,>=0.40.0
23
23
  Requires-Dist: python-dotenv>=1.0.1
24
24
  Requires-Dist: PyYAML>=6.0.2
25
25
  Requires-Dist: pydantic>=2.9.2
@@ -40,7 +40,6 @@ Requires-Dist: azure-mgmt-costmanagement>=4.0.0
40
40
  Requires-Dist: azure-keyvault-secrets>=4.8.0
41
41
  Requires-Dist: openai>=1.55.0
42
42
  Provides-Extra: export
43
- Requires-Dist: weasyprint>=62.0; extra == "export"
44
43
  Requires-Dist: python-pptx>=1.0.0; extra == "export"
45
44
  Provides-Extra: chat
46
45
  Requires-Dist: rich>=13.0.0; extra == "chat"
@@ -101,16 +100,27 @@ Top findings
101
100
 
102
101
  ---
103
102
 
104
- ## 🆕 What's new in v0.6.0
103
+ ## What's new in v0.7.0
105
104
 
106
- > **Breaking:** scheduled scans now create Jira tickets only when `REMEDIATION_ENABLED=true` (default `false`). If you ran v0.5.0 with Jira configured, set it to keep tickets flowing. `argus policies apply --confirm` is unaffected.
105
+ > **A trust release.** Argus now says what it covered, fails loudly when it cannot do its job, and never presents a guess as a bill.
106
+
107
+ > **Breaking:** the decision board is now the only HTML report, and PDF export is removed. Scans, local saves and the S3 / GCS / Azure Blob upload write the board, and the Slack "Full report" link opens it. `REPORT_FORMAT=pdf` writes nothing. The JSON report is the stable interface. Exit code 3 and 4 now signal a failed scan or failed preflight.
107
108
 
108
- - **Dry runs never touch Jira** — `DRY_RUN=true` / `--dry-run` previously still created real tickets when Jira was configured.
109
- - **One summary log line per remediation run**, with `error_type` on failures.
110
- - **`ARGUS_POLICY_DIR`** is read by both the CLI and scheduled scans (`ARGUS_POLICIES_DIR` still accepted).
109
+ - **Honest money.** Every dollar is labelled billed, list price or model estimate. With no billed data the headline reads "unverified waste estimate" and says it is not a savings figure. Model estimates are not invoices; billed numbers need resource-level data in the payer account's Cost Explorer settings.
110
+ - **Coverage and preflight.** Reports state resources discovered vs analysed, regions, the real metrics window and what could not be seen. AWS preflight checks Resource Explorer, tags and the scan role before any model spend.
111
+ - **Fewer false alarms.** A missing metric is "could not judge", not idle: NAT gateways, load balancers and similar resources with no traffic data are withheld from the waste total. ElastiCache failover replicas and AMI-backed or retained snapshots are not recommended for deletion.
112
+ - **Decision board.** Findings group into a short list of opportunities with savings ranges, evidence and check-first steps. `argus board --report FILE.json --client-safe` writes a redacted copy to show a client (read it before sending).
113
+ - **Cost control.** Plan line and estimate before the model runs, strict stop before overspend, scan progress on stderr.
114
+ - **Depth differs by cloud.** AWS is the proven path. GCP and Azure run the same pipeline but have no state rules, preflight or tag fallback yet.
111
115
 
112
116
  [Full changelog →](CHANGELOG.md) · [Project status →](docs/roadmap.md)
113
117
 
118
+ ### v0.6.0
119
+
120
+ > **Breaking:** scheduled scans now create Jira tickets only when `REMEDIATION_ENABLED=true` (default `false`). If you ran v0.5.0 with Jira configured, set it to keep tickets flowing. `argus policies apply --confirm` is unaffected.
121
+
122
+ - **Dry runs never touch Jira**, **one summary log line per remediation run**, and **`ARGUS_POLICY_DIR`** is read by the CLI and scheduled scans.
123
+
114
124
  ### v0.5.0
115
125
 
116
126
  > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
@@ -120,7 +130,7 @@ Top findings
120
130
  - **Rightsizing recommendations** — `resize` and `reduce_nodes` proposals now include a specific target tier or node count derived from observed CPU% (e.g., "Recommend db.t3.small — observed CPU ~8%").
121
131
  - **Acceptance rate monitoring** — `argus policies stats` reads the append-only audit log and prints per-policy proposal counts, Jira new/update splits, and cloud breakdown.
122
132
  - **JSONL audit log** — every `apply --confirm` appends a line to `local_reports/audit.jsonl` mapping `proposal_id → jira_key → jira_url`.
123
- - **1756 tests** — all pass offline, no cloud credentials needed.
133
+ - **2,700+ tests** — all pass offline, no cloud credentials needed.
124
134
 
125
135
  [Full changelog →](CHANGELOG.md)
126
136
 
@@ -133,9 +143,11 @@ Every week (or on demand), Argus:
133
143
  1. **Discovers** every resource in your cloud account using AWS Resource Explorer / GCP Asset Inventory / Azure Resource Graph
134
144
  2. **Analyzes** each candidate — CloudWatch/Cloud Monitoring/Azure Monitor metrics, Cost Explorer/BigQuery/Cost Management cost data, and CloudTrail/Audit Log/Activity Log last-activity timestamps
135
145
  3. **Reasons** about idleness using Claude (via AWS Bedrock, Anthropic API, or Vertex AI) — no hardcoded thresholds
136
- 4. **Reports** a compact digest (Slack, Microsoft Teams, or generic webhook) with top findings and a link to a full self-contained HTML report
146
+ 4. **Reports** a compact digest (Slack, Microsoft Teams, or generic webhook) with top findings and a link to the full decision board (a self-contained HTML page)
137
147
 
138
- The **Full report** button links to a self-contained HTML file (S3 / GCS / Azure Blob) with a filterable/sortable table and expandable AI reasoning per finding. Works offline, no login required.
148
+ The **Full report** button links to the decision board, a self-contained HTML file (S3 / GCS / Azure Blob) that groups findings into opportunities with savings ranges, evidence, check-first steps and Approve / Investigate / Keep. Works offline, no login required.
149
+
150
+ To decide what to do about the findings, `argus board --report <scan>.json` builds a single offline page that groups them into a short list of opportunities, with savings ranges, risk and check-first steps, and Approve / Investigate / Keep buttons. See [`examples/sample-board.html`](examples/sample-board.html) and the [Decision board](docs/concepts/board-report.md) page.
139
151
 
140
152
  > **See realistic examples:** [`sample-report-aws.json`](examples/sample-report-aws.json) · [`sample-report-gcp.json`](examples/sample-report-gcp.json) · [`sample-report-azure.json`](examples/sample-report-azure.json) — 5 findings each with AI-written reasoning, metrics, and cost data.
141
153
 
@@ -253,7 +265,7 @@ docker run --rm \
253
265
 
254
266
  ```bash
255
267
  pip install argus-cloud-optimizer
256
- argus --version # argus 0.6.0
268
+ argus --version # argus 0.7.0
257
269
  argus --help
258
270
  ```
259
271
 
@@ -318,7 +330,7 @@ argus chat # auto-detects cloud
318
330
  ```
319
331
 
320
332
  ```
321
- Argus v0.6.0 — AI Cloud Detective
333
+ Argus v0.7.0 — AI Cloud Detective
322
334
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
323
335
  Tip: end a line with \ to continue on the next line.
324
336
  Type your question, or /help for commands.
@@ -370,6 +382,7 @@ Available REPL commands: `/help`, `/scan`, `/cost`, `/clear`, `/summary`, `/quit
370
382
  ```
371
383
  argus scan [--cloud aws|gcp|azure] [options] # full batch scan
372
384
  argus chat [--cloud aws|gcp|azure] [options] # interactive Q&A
385
+ argus board --report FILE.json [--out PATH] # static decision board page (no cloud access)
373
386
  argus --run-now --cloud aws [options] # backward compat
374
387
 
375
388
  --cloud CLOUD Cloud provider (auto-detected from env vars if omitted)
@@ -378,11 +391,39 @@ argus --run-now --cloud aws [options] # backward compat
378
391
  --ignore-regions REGIONS Comma-separated regions to skip (e.g. ap-east-1,me-south-1)
379
392
  --ai-provider PROVIDER anthropic | bedrock | vertexai | azure_openai (default: anthropic)
380
393
  --accounts PATH Path to accounts.yaml for multi-account/project/subscription mode
381
- --max-resources N Maximum resources to analyze per scan (default: 200)
382
- --lookback-days DAYS Metrics lookback window in days (default: 90, use 14 for faster local dev)
394
+ --max-resources N Maximum resources to analyze per scan (default: scaled from --llm-budget, 300 at $2.00)
395
+ --lookback-days DAYS Metrics lookback window in days, applied to every metrics query and shown in the report (default: 90, use 14 for faster local dev)
383
396
  --llm-budget USD Cost budget per scan/session (default: $2.00 scan, $1.00 chat)
397
+ --cost-days DAYS Per-resource cost window in days (default: 14, the most AWS holds)
398
+ --activity-days DAYS Last-activity (CloudTrail) window in days (default: 90, the most CloudTrail holds)
399
+ --expect-account ID Abort (exit 4) unless the AWS credentials belong to this account
400
+ --strict-coverage Fail (exit 1) if the cap or a search limit leaves resources unanalysed
401
+ --skip-preflight Skip the AWS pre-scan environment checks (recorded in the report)
402
+ --no-progress Do not show scan progress on stderr (also ARGUS_PROGRESS=off)
384
403
  ```
385
404
 
405
+ ### Scan progress
406
+
407
+ `argus scan` reports its stages on stderr while it runs, so a long scan never looks hung. In a terminal it is one live line that updates in place, and log lines scroll above it. In a log file or a background task it is plain, throttled lines:
408
+
409
+ ```text
410
+ [1/5] Discovering resources: 35 searches
411
+ [1/5] Discovering resources: 18/35 (51%) 00:18 elapsed, about 00:17 left, region eu-central-1
412
+ [1/5] Discovering resources: 127/211 (60%) 01:00 elapsed, about 00:40 left, eu-central-1 / lambda
413
+ [1/5] Discovering resources: done, 211 searches in 01:28, 1,420 resources found
414
+ [2/5] Applying rules: done in 00:09, 83 findings from rules
415
+ [3/5] Collecting evidence: 1,337 resources
416
+ [3/5] Collecting evidence: 340/1,337 (25%) 01:12 elapsed, about 03:36 left
417
+ [3/5] Collecting evidence: done, 1,337 resources in 04:51
418
+ LLM plan: 1254 residual candidates after rules (83). Budget covers all of them (estimated $1.13).
419
+ [4/5] Reviewing with the model: 26 batches
420
+ [4/5] Reviewing with the model: done, 26 batches in 06:30, $1.38 of $2.00
421
+ [5/5] Building reports: done in 00:02
422
+ Total time 12:14
423
+ ```
424
+
425
+ Discovery counts its Resource Explorer searches. When a region returns the 1,000-result limit it is split by service, and those searches are added to the total, so the count can grow while it runs; the percentage always means "of the work known so far". stdout stays the JSON result, so `argus scan | jq` still works. Turn it off with `--no-progress` or `ARGUS_PROGRESS=off`; `ARGUS_PROGRESS=plain` forces the line output even in a terminal. Lambda, Cloud Run and Azure Function runs never show it.
426
+
386
427
  ---
387
428
 
388
429
  ## Deploy to AWS Lambda
@@ -539,6 +580,10 @@ Argus needs **read-only** access. The Lambda execution role requires:
539
580
  ```
540
581
  resource-explorer-2:Search
541
582
  resource-explorer-2:GetView
583
+ resource-explorer-2:GetDefaultView
584
+ resource-explorer-2:ListViews
585
+ resource-explorer-2:ListIndexes
586
+ resource-explorer-2:GetIndex
542
587
  cloudwatch:GetMetricData
543
588
  ce:GetCostAndUsage
544
589
  ce:GetCostAndUsageWithResources
@@ -616,7 +661,7 @@ Before you invest time deploying Argus, know what it **can't** do yet:
616
661
  | **Resource discovery** | All three strong | AWS covers 43 resource types via Resource Explorer. GCP covers 31 asset types via Asset Inventory. Azure discovers all types via Resource Graph with 40 having curated metric mappings. All three fall back to dynamic metric auto-discovery for unmapped types. AWS has the deepest per-type metric coverage; GCP and Azure are catching up. |
617
662
  | **Cost accuracy** | Best-effort | AWS Cost Explorer charges $0.01/API call — Argus batches aggressively (max 2 calls/scan). GCP requires BigQuery billing export enabled. Azure cost data depends on subscription-level access. Resource-level cost allocation must be enabled in AWS for per-resource costs; without it, costs show $0.00. |
618
663
  | **AI non-determinism** | By design | The AI decides what's idle — different runs may produce slightly different findings or reasoning. Set `AI_TEMPERATURE=0.0` (default) for most consistent results. |
619
- | **LLM cost** | Configurable | A full scan of ~200 resources costs ~$0.05–$0.50 in LLM API fees depending on provider. Use `--llm-budget` to set a hard cap (default: $2.00/scan). Large estates (1000+ resources) will hit the budget limit — increase it or use `--max-resources`. |
664
+ | **LLM cost** | Configurable | One measured run on 1,614 resources cost about $1.4 to $1.55 (Bedrock, Sonnet-class), roughly $0.85 per 1,000 resources; your figure varies. Use `--llm-budget` to set a hard cap (default: $2.00/scan, about 300 resources). The plan line shows what fits before any model call; raise the budget for larger estates. |
620
665
  | **AWS Resource Explorer setup** | Manual step | You must enable Resource Explorer with an **aggregator index** (typically in `us-east-1`). Without this, Argus cannot discover resources. This is a one-time setup but is easy to miss. |
621
666
  | **Write actions** | None (by design) | Argus is strictly read-only. The policy engine generates Jira tickets with runbooks — a human reviews and executes. Argus never calls the runbook itself. |
622
667
  | **Multi-cloud in one scan** | Not yet | Each `argus` invocation scans one cloud. Use the merge report feature (`core/reports/multi_cloud.py`) to combine results after separate runs. |
@@ -686,7 +731,7 @@ argus/
686
731
  │ ├── aws/ # CloudFormation templates
687
732
  │ ├── gcp/ # Cloud Run + Scheduler deploy script
688
733
  │ └── azure/ # Bicep templates
689
- └── tests/ # 1756 tests, all pass offline
734
+ └── tests/ # 2,700+ tests, all pass offline
690
735
  ```
691
736
 
692
737
  ---
@@ -37,16 +37,27 @@ Top findings
37
37
 
38
38
  ---
39
39
 
40
- ## 🆕 What's new in v0.6.0
40
+ ## What's new in v0.7.0
41
41
 
42
- > **Breaking:** scheduled scans now create Jira tickets only when `REMEDIATION_ENABLED=true` (default `false`). If you ran v0.5.0 with Jira configured, set it to keep tickets flowing. `argus policies apply --confirm` is unaffected.
42
+ > **A trust release.** Argus now says what it covered, fails loudly when it cannot do its job, and never presents a guess as a bill.
43
+
44
+ > **Breaking:** the decision board is now the only HTML report, and PDF export is removed. Scans, local saves and the S3 / GCS / Azure Blob upload write the board, and the Slack "Full report" link opens it. `REPORT_FORMAT=pdf` writes nothing. The JSON report is the stable interface. Exit code 3 and 4 now signal a failed scan or failed preflight.
43
45
 
44
- - **Dry runs never touch Jira** — `DRY_RUN=true` / `--dry-run` previously still created real tickets when Jira was configured.
45
- - **One summary log line per remediation run**, with `error_type` on failures.
46
- - **`ARGUS_POLICY_DIR`** is read by both the CLI and scheduled scans (`ARGUS_POLICIES_DIR` still accepted).
46
+ - **Honest money.** Every dollar is labelled billed, list price or model estimate. With no billed data the headline reads "unverified waste estimate" and says it is not a savings figure. Model estimates are not invoices; billed numbers need resource-level data in the payer account's Cost Explorer settings.
47
+ - **Coverage and preflight.** Reports state resources discovered vs analysed, regions, the real metrics window and what could not be seen. AWS preflight checks Resource Explorer, tags and the scan role before any model spend.
48
+ - **Fewer false alarms.** A missing metric is "could not judge", not idle: NAT gateways, load balancers and similar resources with no traffic data are withheld from the waste total. ElastiCache failover replicas and AMI-backed or retained snapshots are not recommended for deletion.
49
+ - **Decision board.** Findings group into a short list of opportunities with savings ranges, evidence and check-first steps. `argus board --report FILE.json --client-safe` writes a redacted copy to show a client (read it before sending).
50
+ - **Cost control.** Plan line and estimate before the model runs, strict stop before overspend, scan progress on stderr.
51
+ - **Depth differs by cloud.** AWS is the proven path. GCP and Azure run the same pipeline but have no state rules, preflight or tag fallback yet.
47
52
 
48
53
  [Full changelog →](CHANGELOG.md) · [Project status →](docs/roadmap.md)
49
54
 
55
+ ### v0.6.0
56
+
57
+ > **Breaking:** scheduled scans now create Jira tickets only when `REMEDIATION_ENABLED=true` (default `false`). If you ran v0.5.0 with Jira configured, set it to keep tickets flowing. `argus policies apply --confirm` is unaffected.
58
+
59
+ - **Dry runs never touch Jira**, **one summary log line per remediation run**, and **`ARGUS_POLICY_DIR`** is read by the CLI and scheduled scans.
60
+
50
61
  ### v0.5.0
51
62
 
52
63
  > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
@@ -56,7 +67,7 @@ Top findings
56
67
  - **Rightsizing recommendations** — `resize` and `reduce_nodes` proposals now include a specific target tier or node count derived from observed CPU% (e.g., "Recommend db.t3.small — observed CPU ~8%").
57
68
  - **Acceptance rate monitoring** — `argus policies stats` reads the append-only audit log and prints per-policy proposal counts, Jira new/update splits, and cloud breakdown.
58
69
  - **JSONL audit log** — every `apply --confirm` appends a line to `local_reports/audit.jsonl` mapping `proposal_id → jira_key → jira_url`.
59
- - **1756 tests** — all pass offline, no cloud credentials needed.
70
+ - **2,700+ tests** — all pass offline, no cloud credentials needed.
60
71
 
61
72
  [Full changelog →](CHANGELOG.md)
62
73
 
@@ -69,9 +80,11 @@ Every week (or on demand), Argus:
69
80
  1. **Discovers** every resource in your cloud account using AWS Resource Explorer / GCP Asset Inventory / Azure Resource Graph
70
81
  2. **Analyzes** each candidate — CloudWatch/Cloud Monitoring/Azure Monitor metrics, Cost Explorer/BigQuery/Cost Management cost data, and CloudTrail/Audit Log/Activity Log last-activity timestamps
71
82
  3. **Reasons** about idleness using Claude (via AWS Bedrock, Anthropic API, or Vertex AI) — no hardcoded thresholds
72
- 4. **Reports** a compact digest (Slack, Microsoft Teams, or generic webhook) with top findings and a link to a full self-contained HTML report
83
+ 4. **Reports** a compact digest (Slack, Microsoft Teams, or generic webhook) with top findings and a link to the full decision board (a self-contained HTML page)
73
84
 
74
- The **Full report** button links to a self-contained HTML file (S3 / GCS / Azure Blob) with a filterable/sortable table and expandable AI reasoning per finding. Works offline, no login required.
85
+ The **Full report** button links to the decision board, a self-contained HTML file (S3 / GCS / Azure Blob) that groups findings into opportunities with savings ranges, evidence, check-first steps and Approve / Investigate / Keep. Works offline, no login required.
86
+
87
+ To decide what to do about the findings, `argus board --report <scan>.json` builds a single offline page that groups them into a short list of opportunities, with savings ranges, risk and check-first steps, and Approve / Investigate / Keep buttons. See [`examples/sample-board.html`](examples/sample-board.html) and the [Decision board](docs/concepts/board-report.md) page.
75
88
 
76
89
  > **See realistic examples:** [`sample-report-aws.json`](examples/sample-report-aws.json) · [`sample-report-gcp.json`](examples/sample-report-gcp.json) · [`sample-report-azure.json`](examples/sample-report-azure.json) — 5 findings each with AI-written reasoning, metrics, and cost data.
77
90
 
@@ -189,7 +202,7 @@ docker run --rm \
189
202
 
190
203
  ```bash
191
204
  pip install argus-cloud-optimizer
192
- argus --version # argus 0.6.0
205
+ argus --version # argus 0.7.0
193
206
  argus --help
194
207
  ```
195
208
 
@@ -254,7 +267,7 @@ argus chat # auto-detects cloud
254
267
  ```
255
268
 
256
269
  ```
257
- Argus v0.6.0 — AI Cloud Detective
270
+ Argus v0.7.0 — AI Cloud Detective
258
271
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
259
272
  Tip: end a line with \ to continue on the next line.
260
273
  Type your question, or /help for commands.
@@ -306,6 +319,7 @@ Available REPL commands: `/help`, `/scan`, `/cost`, `/clear`, `/summary`, `/quit
306
319
  ```
307
320
  argus scan [--cloud aws|gcp|azure] [options] # full batch scan
308
321
  argus chat [--cloud aws|gcp|azure] [options] # interactive Q&A
322
+ argus board --report FILE.json [--out PATH] # static decision board page (no cloud access)
309
323
  argus --run-now --cloud aws [options] # backward compat
310
324
 
311
325
  --cloud CLOUD Cloud provider (auto-detected from env vars if omitted)
@@ -314,11 +328,39 @@ argus --run-now --cloud aws [options] # backward compat
314
328
  --ignore-regions REGIONS Comma-separated regions to skip (e.g. ap-east-1,me-south-1)
315
329
  --ai-provider PROVIDER anthropic | bedrock | vertexai | azure_openai (default: anthropic)
316
330
  --accounts PATH Path to accounts.yaml for multi-account/project/subscription mode
317
- --max-resources N Maximum resources to analyze per scan (default: 200)
318
- --lookback-days DAYS Metrics lookback window in days (default: 90, use 14 for faster local dev)
331
+ --max-resources N Maximum resources to analyze per scan (default: scaled from --llm-budget, 300 at $2.00)
332
+ --lookback-days DAYS Metrics lookback window in days, applied to every metrics query and shown in the report (default: 90, use 14 for faster local dev)
319
333
  --llm-budget USD Cost budget per scan/session (default: $2.00 scan, $1.00 chat)
334
+ --cost-days DAYS Per-resource cost window in days (default: 14, the most AWS holds)
335
+ --activity-days DAYS Last-activity (CloudTrail) window in days (default: 90, the most CloudTrail holds)
336
+ --expect-account ID Abort (exit 4) unless the AWS credentials belong to this account
337
+ --strict-coverage Fail (exit 1) if the cap or a search limit leaves resources unanalysed
338
+ --skip-preflight Skip the AWS pre-scan environment checks (recorded in the report)
339
+ --no-progress Do not show scan progress on stderr (also ARGUS_PROGRESS=off)
320
340
  ```
321
341
 
342
+ ### Scan progress
343
+
344
+ `argus scan` reports its stages on stderr while it runs, so a long scan never looks hung. In a terminal it is one live line that updates in place, and log lines scroll above it. In a log file or a background task it is plain, throttled lines:
345
+
346
+ ```text
347
+ [1/5] Discovering resources: 35 searches
348
+ [1/5] Discovering resources: 18/35 (51%) 00:18 elapsed, about 00:17 left, region eu-central-1
349
+ [1/5] Discovering resources: 127/211 (60%) 01:00 elapsed, about 00:40 left, eu-central-1 / lambda
350
+ [1/5] Discovering resources: done, 211 searches in 01:28, 1,420 resources found
351
+ [2/5] Applying rules: done in 00:09, 83 findings from rules
352
+ [3/5] Collecting evidence: 1,337 resources
353
+ [3/5] Collecting evidence: 340/1,337 (25%) 01:12 elapsed, about 03:36 left
354
+ [3/5] Collecting evidence: done, 1,337 resources in 04:51
355
+ LLM plan: 1254 residual candidates after rules (83). Budget covers all of them (estimated $1.13).
356
+ [4/5] Reviewing with the model: 26 batches
357
+ [4/5] Reviewing with the model: done, 26 batches in 06:30, $1.38 of $2.00
358
+ [5/5] Building reports: done in 00:02
359
+ Total time 12:14
360
+ ```
361
+
362
+ Discovery counts its Resource Explorer searches. When a region returns the 1,000-result limit it is split by service, and those searches are added to the total, so the count can grow while it runs; the percentage always means "of the work known so far". stdout stays the JSON result, so `argus scan | jq` still works. Turn it off with `--no-progress` or `ARGUS_PROGRESS=off`; `ARGUS_PROGRESS=plain` forces the line output even in a terminal. Lambda, Cloud Run and Azure Function runs never show it.
363
+
322
364
  ---
323
365
 
324
366
  ## Deploy to AWS Lambda
@@ -475,6 +517,10 @@ Argus needs **read-only** access. The Lambda execution role requires:
475
517
  ```
476
518
  resource-explorer-2:Search
477
519
  resource-explorer-2:GetView
520
+ resource-explorer-2:GetDefaultView
521
+ resource-explorer-2:ListViews
522
+ resource-explorer-2:ListIndexes
523
+ resource-explorer-2:GetIndex
478
524
  cloudwatch:GetMetricData
479
525
  ce:GetCostAndUsage
480
526
  ce:GetCostAndUsageWithResources
@@ -552,7 +598,7 @@ Before you invest time deploying Argus, know what it **can't** do yet:
552
598
  | **Resource discovery** | All three strong | AWS covers 43 resource types via Resource Explorer. GCP covers 31 asset types via Asset Inventory. Azure discovers all types via Resource Graph with 40 having curated metric mappings. All three fall back to dynamic metric auto-discovery for unmapped types. AWS has the deepest per-type metric coverage; GCP and Azure are catching up. |
553
599
  | **Cost accuracy** | Best-effort | AWS Cost Explorer charges $0.01/API call — Argus batches aggressively (max 2 calls/scan). GCP requires BigQuery billing export enabled. Azure cost data depends on subscription-level access. Resource-level cost allocation must be enabled in AWS for per-resource costs; without it, costs show $0.00. |
554
600
  | **AI non-determinism** | By design | The AI decides what's idle — different runs may produce slightly different findings or reasoning. Set `AI_TEMPERATURE=0.0` (default) for most consistent results. |
555
- | **LLM cost** | Configurable | A full scan of ~200 resources costs ~$0.05–$0.50 in LLM API fees depending on provider. Use `--llm-budget` to set a hard cap (default: $2.00/scan). Large estates (1000+ resources) will hit the budget limit — increase it or use `--max-resources`. |
601
+ | **LLM cost** | Configurable | One measured run on 1,614 resources cost about $1.4 to $1.55 (Bedrock, Sonnet-class), roughly $0.85 per 1,000 resources; your figure varies. Use `--llm-budget` to set a hard cap (default: $2.00/scan, about 300 resources). The plan line shows what fits before any model call; raise the budget for larger estates. |
556
602
  | **AWS Resource Explorer setup** | Manual step | You must enable Resource Explorer with an **aggregator index** (typically in `us-east-1`). Without this, Argus cannot discover resources. This is a one-time setup but is easy to miss. |
557
603
  | **Write actions** | None (by design) | Argus is strictly read-only. The policy engine generates Jira tickets with runbooks — a human reviews and executes. Argus never calls the runbook itself. |
558
604
  | **Multi-cloud in one scan** | Not yet | Each `argus` invocation scans one cloud. Use the merge report feature (`core/reports/multi_cloud.py`) to combine results after separate runs. |
@@ -622,7 +668,7 @@ argus/
622
668
  │ ├── aws/ # CloudFormation templates
623
669
  │ ├── gcp/ # Cloud Run + Scheduler deploy script
624
670
  │ └── azure/ # Bicep templates
625
- └── tests/ # 1756 tests, all pass offline
671
+ └── tests/ # 2,700+ tests, all pass offline
626
672
  ```
627
673
 
628
674
  ---
@@ -5,8 +5,19 @@ from datetime import datetime
5
5
 
6
6
  import boto3
7
7
 
8
- from adapters.aws import auth, cloudtrail, cloudwatch, cost_explorer, resource_explorer
8
+ from adapters.aws import (
9
+ auth,
10
+ cloudtrail,
11
+ cloudwatch,
12
+ cost_explorer,
13
+ logs_activity,
14
+ resource_explorer,
15
+ )
16
+ from adapters.aws.preflight import run_preflight
17
+ from adapters.aws.state import collect_state
18
+ from adapters.aws.tags import fetch_tags
9
19
  from adapters.base import CloudAdapter, MetricSummary, Resource
20
+ from core.models.preflight import PreflightResult
10
21
 
11
22
 
12
23
  class AWSAdapter(CloudAdapter):
@@ -20,24 +31,60 @@ class AWSAdapter(CloudAdapter):
20
31
  adapter = AWSAdapter(session)
21
32
  """
22
33
 
34
+ # GetCostAndUsageWithResources only holds the last 14 days
35
+ max_cost_days: int | None = 14
36
+
23
37
  def __init__(
24
38
  self,
25
39
  session: boto3.Session,
26
40
  aggregator_region: str | None = None,
27
41
  ) -> None:
28
42
  self._session = session
43
+ self.discovery_complete: bool | None = None
44
+ self.filtered_by_type: dict[str, int] = {}
45
+ # Where the tags came from: "resource_explorer", "tagging_api", "unavailable"
46
+ self.tags_source: str | None = None
29
47
  self._aggregator_region: str = (
30
48
  aggregator_region
31
49
  or os.environ.get("RESOURCE_EXPLORER_REGION")
32
50
  or "us-east-1"
33
51
  )
34
52
 
53
+ def preflight(self, account_id: str | None = None) -> PreflightResult:
54
+ """Read-only environment checks to run before spending any LLM budget."""
55
+ return run_preflight(self._session, self._aggregator_region, account_id)
56
+
35
57
  def list_resources(self, ignore_regions: list[str] | None = None) -> list[Resource]:
36
- return resource_explorer.list_resources(
58
+ found = resource_explorer.discover(
37
59
  session=self._session,
38
60
  ignore_regions=ignore_regions,
39
61
  aggregator_region=self._aggregator_region,
40
62
  )
63
+ self.discovery_complete = found.complete
64
+ self.filtered_by_type = found.filtered_by_type
65
+ self._fill_missing_tags(found.resources)
66
+ return found.resources
67
+
68
+ def _fill_missing_tags(self, resources: list[Resource]) -> None:
69
+ """If Resource Explorer gave no tags at all, ask the Tagging API."""
70
+ if not resources:
71
+ return
72
+ if any(r.tags for r in resources):
73
+ self.tags_source = "resource_explorer"
74
+ return
75
+ fetched = fetch_tags(self._session, resources, self._aggregator_region)
76
+ if fetched is None:
77
+ self.tags_source = "unavailable"
78
+ return
79
+ self.tags_source = "tagging_api"
80
+ for r in resources:
81
+ tags = fetched.get(r.resource_id)
82
+ if tags:
83
+ r.tags = tags
84
+ r.name = r.name or tags.get("Name")
85
+
86
+ def collect_state(self, resources: list[Resource]) -> dict[str, dict]:
87
+ return collect_state(self._session, resources)
41
88
 
42
89
  def get_metrics(
43
90
  self,
@@ -68,6 +115,8 @@ class AWSAdapter(CloudAdapter):
68
115
  resource_id: str,
69
116
  resource_type: str,
70
117
  ) -> datetime | None:
118
+ if logs_activity.is_log_group(resource_type):
119
+ return logs_activity.get_last_event(self._session, resource_id)
71
120
  return cloudtrail.get_last_activity(
72
121
  session=self._session,
73
122
  resource_id=resource_id,
@@ -8,6 +8,7 @@ from botocore.exceptions import ClientError
8
8
 
9
9
  from adapters.aws.config import BOTO_TIMEOUT_CONFIG
10
10
  from adapters.aws.retry import retry_on_transient
11
+ from core.config import get_settings
11
12
 
12
13
  logger = structlog.get_logger(__name__)
13
14
 
@@ -21,7 +22,8 @@ def get_last_activity(
21
22
  ) -> datetime | None:
22
23
  """
23
24
  Return the timestamp of the most recent CloudTrail event for a resource.
24
- Returns None if no activity was found in the last 90 days.
25
+ Returns None if no activity was found in the configured window (default 90
26
+ days, the most CloudTrail LookupEvents can see).
25
27
 
26
28
  Uses the resource name (extracted from ARN) as the lookup attribute
27
29
  since CloudTrail indexes by resource name, not full ARN.
@@ -33,7 +35,8 @@ def get_last_activity(
33
35
  )
34
36
 
35
37
  end_time = datetime.now(tz=timezone.utc)
36
- start_time = end_time - timedelta(days=_LOOKBACK_DAYS)
38
+ window = min(get_settings().scan.activity_lookback_days, _LOOKBACK_DAYS)
39
+ start_time = end_time - timedelta(days=window)
37
40
 
38
41
  try:
39
42
  response = retry_on_transient(
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os as _os
4
+ import threading
5
+ import weakref
6
+ from collections.abc import Iterator
4
7
  from datetime import datetime, timedelta, timezone
5
8
  from typing import Any
6
9
 
@@ -291,7 +294,14 @@ def get_metrics(
291
294
  """
292
295
  metric_defs = _metrics_for(resource_type)
293
296
  if not metric_defs:
294
- metric_defs = _discover_metrics(session, resource_id, resource_type)
297
+ try:
298
+ metric_defs = _discover_metrics(session, resource_id, resource_type)
299
+ except ClientError as exc:
300
+ logger.warning(
301
+ "cloudwatch_list_metrics_failed",
302
+ extra={"resource_id": resource_id, "error": str(exc)},
303
+ )
304
+ return _error_summary(resource_id, resource_type, days, exc)
295
305
  if not metric_defs:
296
306
  return MetricSummary(
297
307
  resource_id=resource_id,
@@ -339,13 +349,7 @@ def get_metrics(
339
349
  "cloudwatch_get_metric_data_failed",
340
350
  extra={"resource_id": resource_id, "error": str(exc)},
341
351
  )
342
- return MetricSummary(
343
- resource_id=resource_id,
344
- resource_type=resource_type,
345
- period_days=days,
346
- metrics={},
347
- has_data=False,
348
- )
352
+ return _error_summary(resource_id, resource_type, days, exc)
349
353
 
350
354
  summary = _parse_results(
351
355
  results=response.get("MetricDataResults", []),
@@ -362,6 +366,21 @@ def get_metrics(
362
366
  return summary
363
367
 
364
368
 
369
+ def _error_summary(
370
+ resource_id: str, resource_type: str, days: int, exc: ClientError
371
+ ) -> MetricSummary:
372
+ code = exc.response.get("Error", {}).get("Code", "ClientError")
373
+ return MetricSummary(
374
+ resource_id=resource_id,
375
+ resource_type=resource_type,
376
+ period_days=days,
377
+ metrics={},
378
+ has_data=False,
379
+ status="error",
380
+ error=f"{code}: {exc.response.get('Error', {}).get('Message', '')}".strip(),
381
+ )
382
+
383
+
365
384
  def _enrich_instance_details(
366
385
  session: boto3.Session,
367
386
  resource_id: str,
@@ -579,59 +598,90 @@ def _parse_results(
579
598
  _FALLBACK_METRIC_LIMIT = 5 # max metrics to auto-discover per unknown resource
580
599
 
581
600
 
601
+ class _RegionListing:
602
+ """One region's ListMetrics result, fetched at most once per session."""
603
+
604
+ def __init__(self) -> None:
605
+ self.lock = threading.Lock()
606
+ self.done = False
607
+ self.index: dict[str, list[tuple[str, str, str]]] = {}
608
+ self.error: ClientError | None = None
609
+
610
+
611
+ _LISTINGS: weakref.WeakKeyDictionary[Any, dict[str, _RegionListing]] = (
612
+ weakref.WeakKeyDictionary()
613
+ )
614
+ _LISTINGS_LOCK = threading.Lock()
615
+
616
+
617
+ def _region_listing(session: boto3.Session, region: str) -> _RegionListing:
618
+ with _LISTINGS_LOCK:
619
+ per_session = _LISTINGS.setdefault(session, {})
620
+ listing = per_session.get(region)
621
+ if listing is None:
622
+ listing = per_session[region] = _RegionListing()
623
+ if listing.done:
624
+ return listing
625
+ with listing.lock:
626
+ if not listing.done:
627
+ client = session.client(
628
+ "cloudwatch", region_name=region, config=BOTO_TIMEOUT_CONFIG
629
+ )
630
+ try:
631
+ for page in _list_metrics_pages(client):
632
+ for m in page.get("Metrics", []):
633
+ for dim in m.get("Dimensions", []):
634
+ listing.index.setdefault(dim.get("Value"), []).append(
635
+ (m["MetricName"], m["Namespace"], dim["Name"])
636
+ )
637
+ except ClientError as exc:
638
+ listing.index = {}
639
+ listing.error = exc
640
+ listing.done = True
641
+ return listing
642
+
643
+
644
+ def _stat_for(metric_name: str) -> str:
645
+ # Sum for count/bytes-sounding names, Average otherwise.
646
+ counting = ("count", "bytes", "records", "invocations", "requests")
647
+ return "Sum" if any(kw in metric_name.lower() for kw in counting) else "Average"
648
+
649
+
582
650
  def _discover_metrics(
583
651
  session: boto3.Session,
584
652
  resource_id: str,
585
653
  resource_type: str,
586
654
  ) -> list[tuple[str, str, str, str]]:
587
655
  """
588
- For resource types not in _METRICS, ask CloudWatch what metrics exist
589
- for this resource and return up to _FALLBACK_METRIC_LIMIT definitions.
656
+ For resource types not in _METRICS, look up which metrics exist for this
657
+ resource in the region's metric list (fetched once per session and region)
658
+ and return up to _FALLBACK_METRIC_LIMIT definitions.
590
659
  Uses the ARN as a dimension value where possible.
660
+ Raises the ClientError that ended the listing, if it failed.
591
661
  """
592
- region = _region_from_arn(resource_id)
593
- client = session.client(
594
- "cloudwatch", region_name=region, config=BOTO_TIMEOUT_CONFIG
595
- )
662
+ listing = _region_listing(session, _region_from_arn(resource_id))
663
+ if listing.error is not None:
664
+ raise listing.error
596
665
 
597
- # Try to find metrics that reference this resource by ARN or last-segment name.
598
666
  dim_value = _dimension_value(resource_id, resource_type)
599
- discovered: list[tuple[str, str, str, str]] = []
667
+ keys = [resource_id] if dim_value == resource_id else [resource_id, dim_value]
668
+ found = [e for k in keys for e in listing.index.get(k, [])]
669
+ return [
670
+ (name, namespace, _stat_for(name), dim_key)
671
+ for name, namespace, dim_key in found[:_FALLBACK_METRIC_LIMIT]
672
+ ]
600
673
 
601
- try:
602
- paginator = client.get_paginator("list_metrics")
603
- for page in paginator.paginate():
604
- for m in page.get("Metrics", []):
605
- for dim in m.get("Dimensions", []):
606
- if dim.get("Value") in (resource_id, dim_value):
607
- metric_name: str = m["MetricName"]
608
- namespace: str = m["Namespace"]
609
- dim_key: str = dim["Name"]
610
- # Use Sum for count/bytes-sounding names, Average otherwise.
611
- stat = (
612
- "Sum"
613
- if any(
614
- kw in metric_name.lower()
615
- for kw in (
616
- "count",
617
- "bytes",
618
- "records",
619
- "invocations",
620
- "requests",
621
- )
622
- )
623
- else "Average"
624
- )
625
- discovered.append((metric_name, namespace, stat, dim_key))
626
- if len(discovered) >= _FALLBACK_METRIC_LIMIT:
627
- return discovered
628
- except ClientError as exc:
629
- logger.warning(
630
- "cloudwatch_list_metrics_failed",
631
- extra={"resource_id": resource_id, "error": str(exc)},
632
- )
633
674
 
634
- return discovered
675
+ def _list_metrics_pages(client: Any) -> Iterator[dict[str, Any]]:
676
+ """Yield ListMetrics pages, retrying each call on throttling."""
677
+ kwargs: dict[str, Any] = {}
678
+ while True:
679
+ page = retry_on_transient(client.list_metrics, **kwargs)
680
+ yield page
681
+ token = page.get("NextToken")
682
+ if not token:
683
+ return
684
+ kwargs["NextToken"] = token
635
685
 
636
686
 
637
687
  def _region_from_arn(arn: str) -> str: