argus-cloud-optimizer 0.5.0__tar.gz → 0.6.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 (131) hide show
  1. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/.env.example +6 -2
  2. {argus_cloud_optimizer-0.5.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.6.0}/PKG-INFO +35 -21
  3. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/README.md +34 -20
  4. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/cost_management.py +1 -1
  5. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/monitor.py +0 -2
  6. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0/argus_cloud_optimizer.egg-info}/PKG-INFO +35 -21
  7. argus_cloud_optimizer-0.6.0/core/__version__.py +1 -0
  8. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/agent/loop.py +38 -1
  9. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/config.py +16 -0
  10. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/models/finding.py +8 -0
  11. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/engine.py +19 -13
  12. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/loader.py +27 -0
  13. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/generator.py +5 -1
  14. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/multi_cloud.py +4 -2
  15. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/blog/posts/react-vs-rule-based.md +13 -7
  16. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/concepts/adapters.md +15 -12
  17. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/concepts/ai-providers.md +3 -3
  18. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/concepts/ai-reasoning.md +11 -10
  19. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/concepts/how-it-works.md +28 -28
  20. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/contributing/development.md +25 -9
  21. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/contributing/index.md +2 -2
  22. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/contributing/new-adapter.md +37 -19
  23. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/contributing/new-ai-provider.md +12 -1
  24. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/deployment/aws.md +20 -11
  25. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/deployment/azure.md +17 -12
  26. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/deployment/gcp.md +15 -5
  27. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/deployment/multi-account.md +9 -11
  28. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/getting-started/configuration.md +15 -6
  29. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/getting-started/first-scan.md +7 -6
  30. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/getting-started/index.md +2 -2
  31. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/getting-started/quickstart.md +18 -9
  32. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/index.md +24 -27
  33. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/reference/env-vars.md +17 -9
  34. argus_cloud_optimizer-0.6.0/docs/reference/iam-permissions.md +173 -0
  35. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/reference/report-schema.md +15 -5
  36. argus_cloud_optimizer-0.6.0/docs/reference/resource-types.md +139 -0
  37. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/reference/security.md +19 -11
  38. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/reference/troubleshooting.md +24 -11
  39. argus_cloud_optimizer-0.6.0/docs/remediation/cli.md +215 -0
  40. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/remediation/index.md +1 -0
  41. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/remediation/jira.md +32 -14
  42. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/remediation/policies.md +29 -17
  43. argus_cloud_optimizer-0.6.0/docs/roadmap.md +118 -0
  44. argus_cloud_optimizer-0.6.0/entrypoints/_remediation.py +159 -0
  45. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/cli.py +7 -3
  46. argus_cloud_optimizer-0.5.0/core/__version__.py +0 -1
  47. argus_cloud_optimizer-0.5.0/docs/reference/iam-permissions.md +0 -145
  48. argus_cloud_optimizer-0.5.0/docs/reference/resource-types.md +0 -103
  49. argus_cloud_optimizer-0.5.0/docs/remediation/cli.md +0 -174
  50. argus_cloud_optimizer-0.5.0/docs/roadmap.md +0 -87
  51. argus_cloud_optimizer-0.5.0/entrypoints/_remediation.py +0 -73
  52. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/LICENSE +0 -0
  53. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/MANIFEST.in +0 -0
  54. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/accounts.yaml.example +0 -0
  55. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/__init__.py +0 -0
  56. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/__init__.py +0 -0
  57. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/adapter.py +0 -0
  58. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/auth.py +0 -0
  59. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/cloudtrail.py +0 -0
  60. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/cloudwatch.py +0 -0
  61. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/config.py +0 -0
  62. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/cost_explorer.py +0 -0
  63. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/resource_explorer.py +0 -0
  64. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/aws/retry.py +0 -0
  65. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/__init__.py +0 -0
  66. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/activity_log.py +0 -0
  67. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/adapter.py +0 -0
  68. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/resource_graph.py +0 -0
  69. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/azure/retry.py +0 -0
  70. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/base.py +0 -0
  71. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/__init__.py +0 -0
  72. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/adapter.py +0 -0
  73. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/asset_inventory.py +0 -0
  74. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/billing.py +0 -0
  75. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/cloud_logging.py +0 -0
  76. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/cloud_monitoring.py +0 -0
  77. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/adapters/gcp/retry.py +0 -0
  78. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/__init__.py +0 -0
  79. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/anthropic.py +0 -0
  80. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/azure_openai.py +0 -0
  81. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/base.py +0 -0
  82. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/bedrock.py +0 -0
  83. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/ai/vertexai.py +0 -0
  84. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/argus_cloud_optimizer.egg-info/SOURCES.txt +0 -0
  85. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/argus_cloud_optimizer.egg-info/dependency_links.txt +0 -0
  86. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/argus_cloud_optimizer.egg-info/entry_points.txt +0 -0
  87. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/argus_cloud_optimizer.egg-info/requires.txt +0 -0
  88. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/argus_cloud_optimizer.egg-info/top_level.txt +0 -0
  89. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/__init__.py +0 -0
  90. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/agent/__init__.py +0 -0
  91. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/agent/chat.py +0 -0
  92. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/agent/prompts.py +0 -0
  93. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/log.py +0 -0
  94. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/models/__init__.py +0 -0
  95. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/py.typed +0 -0
  96. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/__init__.py +0 -0
  97. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/aws.py +0 -0
  98. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/azure.py +0 -0
  99. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/factory.py +0 -0
  100. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/gcp.py +0 -0
  101. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/models.py +0 -0
  102. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/registry/registry.py +0 -0
  103. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/__init__.py +0 -0
  104. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/audit.py +0 -0
  105. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/models.py +0 -0
  106. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/rightsizing.py +0 -0
  107. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/remediation/validator.py +0 -0
  108. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/__init__.py +0 -0
  109. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/comparison.py +0 -0
  110. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/delivery.py +0 -0
  111. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/export.py +0 -0
  112. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/reports/html.py +0 -0
  113. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/secrets.py +0 -0
  114. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/token_tracker.py +0 -0
  115. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/core/validation.py +0 -0
  116. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/deploy/aws/multi-account/hub/template.yaml +0 -0
  117. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/deploy/aws/multi-account/spoke-role.yaml +0 -0
  118. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/deploy/aws/single-account/template.yaml +0 -0
  119. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/deploy/azure/function-app.bicep +0 -0
  120. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/deploy/gcp/deploy.sh +0 -0
  121. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/blog/index.md +0 -0
  122. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/concepts/index.md +0 -0
  123. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/deployment/index.md +0 -0
  124. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/docs/reference/index.md +0 -0
  125. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/__init__.py +0 -0
  126. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/aws_lambda.py +0 -0
  127. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/azure_function.py +0 -0
  128. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/cli_chat.py +0 -0
  129. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/entrypoints/gcp_cloudrun.py +0 -0
  130. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/pyproject.toml +0 -0
  131. {argus_cloud_optimizer-0.5.0 → argus_cloud_optimizer-0.6.0}/setup.cfg +0 -0
@@ -70,11 +70,13 @@ DRY_RUN=false
70
70
 
71
71
  # -----------------------------------------------------------------------------
72
72
  # Remediation — Policy Engine + Jira Integration
73
- # Policies are YAML files in ARGUS_POLICIES_DIR. Argus never executes changes;
73
+ # Policies are YAML files in ARGUS_POLICY_DIR. Argus never executes changes;
74
74
  # it creates Jira tickets for human review. See config/policies/ for examples
75
75
  # and CONTRIBUTING.md for how to write policies.
76
76
  # -----------------------------------------------------------------------------
77
- ARGUS_POLICIES_DIR=./config/policies # directory containing policy YAML files
77
+ # Opt-in: create Jira tickets automatically after every scan (default: false).
78
+ # `argus policies apply --confirm` works without this; it is already explicit.
79
+ REMEDIATION_ENABLED=false
78
80
 
79
81
  # Jira credentials — required for `argus policies apply --confirm`
80
82
  # Create an API token at: https://id.atlassian.com/manage-profile/security/api-tokens
@@ -84,6 +86,8 @@ JIRA_API_TOKEN= # Atlassian API token (not your pass
84
86
 
85
87
  # Directory containing your *.yaml policy files (one policy per file).
86
88
  # Copy from config/policies.example/ to get started, then customise.
89
+ # Used by both `argus policies ...` and scheduled scans. The older name
90
+ # ARGUS_POLICIES_DIR is still accepted; if both are set, this one wins.
87
91
  ARGUS_POLICY_DIR=./config/policies
88
92
 
89
93
  # Path to integrations config (project key, labels, priority map).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-cloud-optimizer
3
- Version: 0.5.0
3
+ Version: 0.6.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
@@ -101,7 +101,17 @@ Top findings
101
101
 
102
102
  ---
103
103
 
104
- ## 🆕 What's new in v0.5.0
104
+ ## 🆕 What's new in v0.6.0
105
+
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.
107
+
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).
111
+
112
+ [Full changelog →](CHANGELOG.md) · [Project status →](docs/roadmap.md)
113
+
114
+ ### v0.5.0
105
115
 
106
116
  > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
107
117
 
@@ -159,25 +169,29 @@ argus policies apply --report scan_report.json --confirm
159
169
  argus policies stats # acceptance rate per policy from audit log
160
170
  ```
161
171
 
172
+ To create tickets automatically after every scheduled scan (Lambda, Cloud Run, Azure Function), set `REMEDIATION_ENABLED=true`. It is off by default; `apply --confirm` above works without it.
173
+
162
174
  ### Bundled policies (13)
163
175
 
164
- | Policy | Cloud | Action | Cost threshold |
165
- |--------|-------|--------|---------------|
166
- | `aws-ec2-stop-idle-14d` | AWS | stop | $20/mo |
167
- | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo |
168
- | `aws-ebs-delete-unattached-30d` | AWS | delete | $5/mo |
169
- | `aws-elb-delete-idle` | AWS | delete | $20/mo |
170
- | `aws-elasticache-delete-idle-30d` | AWS | delete | $50/mo |
171
- | `aws-lambda-delete-unused-30d` | AWS | delete | $0 |
172
- | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo |
173
- | `gcp-compute-stop-idle-7d` | GCP | stop | $20/mo |
174
- | `gcp-sql-stop-idle-14d` | GCP | stop | $20/mo |
175
- | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo |
176
- | `azure-vm-stop-idle-14d` | Azure | stop | $20/mo |
177
- | `azure-sql-resize-underutilised` | Azure | resize | $50/mo |
178
- | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo |
179
-
180
- All policies exclude `environment: prod` and `argus-exempt: "true"` tags by default. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
176
+ | Policy | Cloud | Action | Cost floor | Idle days |
177
+ |--------|-------|--------|-----------|-----------|
178
+ | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo | 14 |
179
+ | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo | — |
180
+ | `aws-ebs-delete-unattached-30d` | AWS | snapshot_delete | — | 30 |
181
+ | `aws-elasticache-delete-idle-30d` | AWS | delete | — | 30 |
182
+ | `aws-elb-delete-idle` | AWS | delete | $10/mo | — |
183
+ | `aws-ec2-stop-idle-14d` | AWS | stop | — | 14 |
184
+ | `aws-lambda-delete-unused-30d` | AWS | delete | — | 30 |
185
+ | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo | — |
186
+ | `gcp-sql-stop-idle-14d` | GCP | stop | — | 14 |
187
+ | `gcp-compute-stop-idle-7d` | GCP | stop | — | 7 |
188
+ | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo | — |
189
+ | `azure-sql-resize-underutilised` | Azure | resize | $50/mo | — |
190
+ | `azure-vm-stop-idle-14d` | Azure | stop | — | 14 |
191
+
192
+ Each policy also has AI-priority and metric conditions; see [Policies](https://vamshisiddarth.github.io/argus/remediation/policies/#bundled-policies) for the full table.
193
+
194
+ All policies skip resources tagged `environment: prod` (or `production`) and resources tagged `argus-exempt: "true"` — either tag on its own is enough. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
181
195
 
182
196
  ### Safety guarantees
183
197
 
@@ -239,7 +253,7 @@ docker run --rm \
239
253
 
240
254
  ```bash
241
255
  pip install argus-cloud-optimizer
242
- argus --version # argus 0.5.0
256
+ argus --version # argus 0.6.0
243
257
  argus --help
244
258
  ```
245
259
 
@@ -304,7 +318,7 @@ argus chat # auto-detects cloud
304
318
  ```
305
319
 
306
320
  ```
307
- Argus v0.5.0 — AI Cloud Detective
321
+ Argus v0.6.0 — AI Cloud Detective
308
322
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
309
323
  Tip: end a line with \ to continue on the next line.
310
324
  Type your question, or /help for commands.
@@ -37,7 +37,17 @@ Top findings
37
37
 
38
38
  ---
39
39
 
40
- ## 🆕 What's new in v0.5.0
40
+ ## 🆕 What's new in v0.6.0
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.
43
+
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).
47
+
48
+ [Full changelog →](CHANGELOG.md) · [Project status →](docs/roadmap.md)
49
+
50
+ ### v0.5.0
41
51
 
42
52
  > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
43
53
 
@@ -95,25 +105,29 @@ argus policies apply --report scan_report.json --confirm
95
105
  argus policies stats # acceptance rate per policy from audit log
96
106
  ```
97
107
 
108
+ To create tickets automatically after every scheduled scan (Lambda, Cloud Run, Azure Function), set `REMEDIATION_ENABLED=true`. It is off by default; `apply --confirm` above works without it.
109
+
98
110
  ### Bundled policies (13)
99
111
 
100
- | Policy | Cloud | Action | Cost threshold |
101
- |--------|-------|--------|---------------|
102
- | `aws-ec2-stop-idle-14d` | AWS | stop | $20/mo |
103
- | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo |
104
- | `aws-ebs-delete-unattached-30d` | AWS | delete | $5/mo |
105
- | `aws-elb-delete-idle` | AWS | delete | $20/mo |
106
- | `aws-elasticache-delete-idle-30d` | AWS | delete | $50/mo |
107
- | `aws-lambda-delete-unused-30d` | AWS | delete | $0 |
108
- | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo |
109
- | `gcp-compute-stop-idle-7d` | GCP | stop | $20/mo |
110
- | `gcp-sql-stop-idle-14d` | GCP | stop | $20/mo |
111
- | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo |
112
- | `azure-vm-stop-idle-14d` | Azure | stop | $20/mo |
113
- | `azure-sql-resize-underutilised` | Azure | resize | $50/mo |
114
- | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo |
115
-
116
- All policies exclude `environment: prod` and `argus-exempt: "true"` tags by default. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
112
+ | Policy | Cloud | Action | Cost floor | Idle days |
113
+ |--------|-------|--------|-----------|-----------|
114
+ | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo | 14 |
115
+ | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo | — |
116
+ | `aws-ebs-delete-unattached-30d` | AWS | snapshot_delete | — | 30 |
117
+ | `aws-elasticache-delete-idle-30d` | AWS | delete | — | 30 |
118
+ | `aws-elb-delete-idle` | AWS | delete | $10/mo | — |
119
+ | `aws-ec2-stop-idle-14d` | AWS | stop | — | 14 |
120
+ | `aws-lambda-delete-unused-30d` | AWS | delete | — | 30 |
121
+ | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo | — |
122
+ | `gcp-sql-stop-idle-14d` | GCP | stop | — | 14 |
123
+ | `gcp-compute-stop-idle-7d` | GCP | stop | — | 7 |
124
+ | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo | — |
125
+ | `azure-sql-resize-underutilised` | Azure | resize | $50/mo | — |
126
+ | `azure-vm-stop-idle-14d` | Azure | stop | — | 14 |
127
+
128
+ Each policy also has AI-priority and metric conditions; see [Policies](https://vamshisiddarth.github.io/argus/remediation/policies/#bundled-policies) for the full table.
129
+
130
+ All policies skip resources tagged `environment: prod` (or `production`) and resources tagged `argus-exempt: "true"` — either tag on its own is enough. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
117
131
 
118
132
  ### Safety guarantees
119
133
 
@@ -175,7 +189,7 @@ docker run --rm \
175
189
 
176
190
  ```bash
177
191
  pip install argus-cloud-optimizer
178
- argus --version # argus 0.5.0
192
+ argus --version # argus 0.6.0
179
193
  argus --help
180
194
  ```
181
195
 
@@ -240,7 +254,7 @@ argus chat # auto-detects cloud
240
254
  ```
241
255
 
242
256
  ```
243
- Argus v0.5.0 — AI Cloud Detective
257
+ Argus v0.6.0 — AI Cloud Detective
244
258
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
245
259
  Tip: end a line with \ to continue on the next line.
246
260
  Type your question, or /help for commands.
@@ -102,7 +102,7 @@ def _query_batch(
102
102
  aggregation={"totalCost": {"name": "PreTaxCost", "function": "Sum"}}, # type: ignore[dict-item]
103
103
  grouping=[QueryGrouping(type="Dimension", name="ResourceId")],
104
104
  filter=QueryFilter(
105
- dimensions=QueryComparisonExpression(
105
+ dimensions=QueryComparisonExpression( # type: ignore[call-overload]
106
106
  name="ResourceId",
107
107
  operator="In",
108
108
  values=resource_ids,
@@ -372,8 +372,6 @@ def _discover_metrics(
372
372
  credential: Any = None,
373
373
  ) -> list[tuple[str, str]]:
374
374
  """Auto-discover available metrics for unknown Azure resource types."""
375
- from azure.monitor.query import MetricsQueryClient
376
-
377
375
  cred = credential or DefaultAzureCredential()
378
376
  client = MetricsQueryClient(cred, connection_timeout=10, read_timeout=60)
379
377
  discovered: list[tuple[str, str]] = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-cloud-optimizer
3
- Version: 0.5.0
3
+ Version: 0.6.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
@@ -101,7 +101,17 @@ Top findings
101
101
 
102
102
  ---
103
103
 
104
- ## 🆕 What's new in v0.5.0
104
+ ## 🆕 What's new in v0.6.0
105
+
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.
107
+
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).
111
+
112
+ [Full changelog →](CHANGELOG.md) · [Project status →](docs/roadmap.md)
113
+
114
+ ### v0.5.0
105
115
 
106
116
  > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
107
117
 
@@ -159,25 +169,29 @@ argus policies apply --report scan_report.json --confirm
159
169
  argus policies stats # acceptance rate per policy from audit log
160
170
  ```
161
171
 
172
+ To create tickets automatically after every scheduled scan (Lambda, Cloud Run, Azure Function), set `REMEDIATION_ENABLED=true`. It is off by default; `apply --confirm` above works without it.
173
+
162
174
  ### Bundled policies (13)
163
175
 
164
- | Policy | Cloud | Action | Cost threshold |
165
- |--------|-------|--------|---------------|
166
- | `aws-ec2-stop-idle-14d` | AWS | stop | $20/mo |
167
- | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo |
168
- | `aws-ebs-delete-unattached-30d` | AWS | delete | $5/mo |
169
- | `aws-elb-delete-idle` | AWS | delete | $20/mo |
170
- | `aws-elasticache-delete-idle-30d` | AWS | delete | $50/mo |
171
- | `aws-lambda-delete-unused-30d` | AWS | delete | $0 |
172
- | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo |
173
- | `gcp-compute-stop-idle-7d` | GCP | stop | $20/mo |
174
- | `gcp-sql-stop-idle-14d` | GCP | stop | $20/mo |
175
- | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo |
176
- | `azure-vm-stop-idle-14d` | Azure | stop | $20/mo |
177
- | `azure-sql-resize-underutilised` | Azure | resize | $50/mo |
178
- | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo |
179
-
180
- All policies exclude `environment: prod` and `argus-exempt: "true"` tags by default. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
176
+ | Policy | Cloud | Action | Cost floor | Idle days |
177
+ |--------|-------|--------|-----------|-----------|
178
+ | `aws-redshift-snapshot-delete-14d` | AWS | snapshot_delete | $100/mo | 14 |
179
+ | `aws-rds-resize-high-cost-idle` | AWS | resize | $100/mo | — |
180
+ | `aws-ebs-delete-unattached-30d` | AWS | snapshot_delete | — | 30 |
181
+ | `aws-elasticache-delete-idle-30d` | AWS | delete | — | 30 |
182
+ | `aws-elb-delete-idle` | AWS | delete | $10/mo | — |
183
+ | `aws-ec2-stop-idle-14d` | AWS | stop | — | 14 |
184
+ | `aws-lambda-delete-unused-30d` | AWS | delete | — | 30 |
185
+ | `gcp-gke-reduce-nodes-underutilised` | GCP | reduce_nodes | $200/mo | — |
186
+ | `gcp-sql-stop-idle-14d` | GCP | stop | — | 14 |
187
+ | `gcp-compute-stop-idle-7d` | GCP | stop | — | 7 |
188
+ | `azure-aks-reduce-nodes-underutilised` | Azure | reduce_nodes | $150/mo | — |
189
+ | `azure-sql-resize-underutilised` | Azure | resize | $50/mo | — |
190
+ | `azure-vm-stop-idle-14d` | Azure | stop | — | 14 |
191
+
192
+ Each policy also has AI-priority and metric conditions; see [Policies](https://vamshisiddarth.github.io/argus/remediation/policies/#bundled-policies) for the full table.
193
+
194
+ All policies skip resources tagged `environment: prod` (or `production`) and resources tagged `argus-exempt: "true"` — either tag on its own is enough. Add your own in `config/policies/` — see [CONTRIBUTING.md](CONTRIBUTING.md).
181
195
 
182
196
  ### Safety guarantees
183
197
 
@@ -239,7 +253,7 @@ docker run --rm \
239
253
 
240
254
  ```bash
241
255
  pip install argus-cloud-optimizer
242
- argus --version # argus 0.5.0
256
+ argus --version # argus 0.6.0
243
257
  argus --help
244
258
  ```
245
259
 
@@ -304,7 +318,7 @@ argus chat # auto-detects cloud
304
318
  ```
305
319
 
306
320
  ```
307
- Argus v0.5.0 — AI Cloud Detective
321
+ Argus v0.6.0 — AI Cloud Detective
308
322
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
309
323
  Tip: end a line with \ to continue on the next line.
310
324
  Type your question, or /help for commands.
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -164,7 +164,8 @@ class AgentLoop:
164
164
  findings_count=len(tc.arguments.get("findings", [])),
165
165
  **self.tracker.summary(),
166
166
  )
167
- return _parse_findings(tc.arguments, cloud=cloud)
167
+ findings, summary = _parse_findings(tc.arguments, cloud=cloud)
168
+ return _attach_accounts(findings, accounts), summary
168
169
 
169
170
  # Persist the assistant turn before executing tools
170
171
  messages.append(
@@ -471,6 +472,42 @@ def _compress_resource(r: dict) -> dict:
471
472
  return out
472
473
 
473
474
 
475
+ def _attach_accounts(
476
+ findings: list[ResourceFinding], accounts: list[dict[str, Any]]
477
+ ) -> list[ResourceFinding]:
478
+ """
479
+ Stamp each finding with the account/project/subscription it came from.
480
+
481
+ One account per run (AWS, GCP, single-subscription Azure): every finding
482
+ belongs to it. Several accounts in one run (multi-subscription Azure):
483
+ match on the ``/subscriptions/<id>/`` segment of the resource ID.
484
+ Findings that can't be attributed keep ``account_id=None``.
485
+ """
486
+ if not accounts:
487
+ return findings
488
+ names = {str(a.get("id")): a.get("name") for a in accounts if a.get("id")}
489
+ only = next(iter(names)) if len(names) == 1 else None
490
+ for f in findings:
491
+ acct = only or _subscription_from_resource_id(f.resource_id, names)
492
+ if acct:
493
+ f.account_id = acct
494
+ f.account_name = names.get(acct) or acct
495
+ return findings
496
+
497
+
498
+ def _subscription_from_resource_id(
499
+ resource_id: str, known: dict[str, Any]
500
+ ) -> str | None:
501
+ parts = resource_id.split("/")
502
+ for i, part in enumerate(parts[:-1]):
503
+ if part.lower() == "subscriptions":
504
+ sub = parts[i + 1].lower()
505
+ for acct in known:
506
+ if acct.lower() == sub:
507
+ return acct
508
+ return None
509
+
510
+
474
511
  def _parse_findings(
475
512
  args: dict[str, Any],
476
513
  cloud: str,
@@ -159,6 +159,19 @@ class ReportSettings(BaseSettings):
159
159
  )
160
160
 
161
161
 
162
+ # ---------------------------------------------------------------------------
163
+ # Remediation
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ class RemediationSettings(BaseSettings):
168
+ model_config = SettingsConfigDict(env_prefix="", env_file=".env", extra="ignore")
169
+
170
+ # Opt-in: the post-scan runner creates Jira tickets only when this is true.
171
+ # `argus policies apply --confirm` is already explicit and is not gated.
172
+ enabled: bool = Field(False, alias="REMEDIATION_ENABLED")
173
+
174
+
162
175
  # ---------------------------------------------------------------------------
163
176
  # Scan tuning
164
177
  # ---------------------------------------------------------------------------
@@ -212,6 +225,9 @@ class Settings(BaseSettings):
212
225
  report: ReportSettings = Field(default_factory=ReportSettings) # type: ignore[arg-type]
213
226
  scan: ScanSettings = Field(default_factory=ScanSettings) # type: ignore[arg-type]
214
227
  log: LogSettings = Field(default_factory=LogSettings) # type: ignore[arg-type]
228
+ remediation: RemediationSettings = Field(
229
+ default_factory=RemediationSettings # type: ignore[arg-type]
230
+ )
215
231
 
216
232
 
217
233
  @lru_cache(maxsize=1)
@@ -31,6 +31,10 @@ class ResourceFinding:
31
31
  name: str | None = None
32
32
  last_activity: datetime | None = None
33
33
  status: FindingStatus = "new"
34
+ # Set by the agent loop from the scanned account/project/subscription.
35
+ # Used by remediation `accounts` scope filters and runbook templates.
36
+ account_id: str | None = None
37
+ account_name: str | None = None
34
38
 
35
39
  def to_dict(self) -> dict[str, Any]:
36
40
  return {
@@ -50,6 +54,8 @@ class ResourceFinding:
50
54
  ),
51
55
  "scan_time": self.scan_time.isoformat(),
52
56
  "status": self.status,
57
+ "account_id": self.account_id,
58
+ "account_name": self.account_name,
53
59
  }
54
60
 
55
61
  @classmethod
@@ -73,4 +79,6 @@ class ResourceFinding:
73
79
  last_activity=last_activity,
74
80
  scan_time=scan_time,
75
81
  status=data.get("status", "new"),
82
+ account_id=data.get("account_id"),
83
+ account_name=data.get("account_name"),
76
84
  )
@@ -91,8 +91,7 @@ def _scope_includes(finding: ResourceFinding, scope: ScopeFilter) -> bool:
91
91
  if scope.cloud_platforms and finding.cloud not in scope.cloud_platforms:
92
92
  return False
93
93
 
94
- account = getattr(finding, "account_id", None) or getattr(finding, "cloud", "")
95
- if scope.accounts and account not in scope.accounts:
94
+ if scope.accounts and finding.account_id not in scope.accounts:
96
95
  return False
97
96
 
98
97
  if scope.regions and finding.region not in scope.regions:
@@ -108,30 +107,37 @@ def _scope_excludes(finding: ResourceFinding, scope: ScopeFilter) -> bool:
108
107
  if scope.cloud_platforms and finding.cloud in scope.cloud_platforms:
109
108
  return True
110
109
 
111
- account = getattr(finding, "account_id", None) or getattr(finding, "cloud", "")
112
- if scope.accounts and account in scope.accounts:
110
+ if scope.accounts and finding.account_id in scope.accounts:
113
111
  return True
114
112
 
115
113
  if scope.regions and finding.region in scope.regions:
116
114
  return True
117
115
 
118
- if scope.tags and _tags_match(finding.tags, scope.tags):
116
+ if scope.tags and _any_tag_entry_matches(finding.tags, scope.tags):
119
117
  return True
120
118
 
121
119
  return False
122
120
 
123
121
 
122
+ def _tag_entry_matches(resource_tags: dict, tag_entry: dict[str, list[str]]) -> bool:
123
+ """True if the resource satisfies every key in one tag entry."""
124
+ return all(resource_tags.get(key) in allowed for key, allowed in tag_entry.items())
125
+
126
+
124
127
  def _tags_match(
125
128
  resource_tags: dict,
126
129
  scope_tags: tuple[dict[str, list[str]], ...],
127
130
  ) -> bool:
128
- """Return True if the resource has ALL tag conditions in scope_tags."""
129
- for tag_entry in scope_tags:
130
- for key, allowed_values in tag_entry.items():
131
- resource_value = resource_tags.get(key)
132
- if resource_value not in allowed_values:
133
- return False
134
- return True
131
+ """Include semantics: the resource must satisfy ALL tag entries."""
132
+ return all(_tag_entry_matches(resource_tags, entry) for entry in scope_tags)
133
+
134
+
135
+ def _any_tag_entry_matches(
136
+ resource_tags: dict,
137
+ scope_tags: tuple[dict[str, list[str]], ...],
138
+ ) -> bool:
139
+ """Exclude semantics: matching ANY tag entry excludes the resource."""
140
+ return any(_tag_entry_matches(resource_tags, entry) for entry in scope_tags)
135
141
 
136
142
 
137
143
  def _tier1_matches(finding: ResourceFinding, cond: Condition) -> bool:
@@ -212,7 +218,7 @@ def _build_proposal(finding: ResourceFinding, policy: Policy) -> ChangeProposal:
212
218
  action=policy.action,
213
219
  resource_id=finding.resource_id,
214
220
  region=finding.region,
215
- account_id=getattr(finding, "account_id", None) or "",
221
+ account_id=finding.account_id or "",
216
222
  )
217
223
  or f"# No CLI template available for {finding.resource_type} / {policy.action}"
218
224
  )
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import os
4
5
  from pathlib import Path
5
6
 
6
7
  import yaml
@@ -30,6 +31,32 @@ _VALID_ACTIONS = frozenset(
30
31
 
31
32
  _SUPPORTED_VERSIONS = frozenset({"1"})
32
33
 
34
+ DEFAULT_POLICY_DIR = "./config/policies"
35
+ POLICY_DIR_ENV = "ARGUS_POLICY_DIR"
36
+ # Older name used by the CLI and .env.example before v0.6.0. Still honored.
37
+ POLICY_DIR_ENV_ALIAS = "ARGUS_POLICIES_DIR"
38
+
39
+
40
+ def resolve_policy_dir() -> str:
41
+ """
42
+ Return the policy directory from the environment.
43
+
44
+ Precedence: ARGUS_POLICY_DIR, then ARGUS_POLICIES_DIR, then
45
+ ./config/policies. Used by both the CLI and the post-scan runner so
46
+ `argus policies plan` and a scheduled scan always read the same folder.
47
+ """
48
+ primary = os.environ.get(POLICY_DIR_ENV, "").strip()
49
+ alias = os.environ.get(POLICY_DIR_ENV_ALIAS, "").strip()
50
+ if primary and alias and primary != alias:
51
+ logger.warning(
52
+ "policy_dir_env_conflict using=%s=%s ignored=%s=%s",
53
+ POLICY_DIR_ENV,
54
+ primary,
55
+ POLICY_DIR_ENV_ALIAS,
56
+ alias,
57
+ )
58
+ return primary or alias or DEFAULT_POLICY_DIR
59
+
33
60
 
34
61
  def load_policies(policies_dir: str | Path) -> list[Policy]:
35
62
  """
@@ -17,6 +17,10 @@ def _registry() -> ResourceRegistry:
17
17
  return get_registry()
18
18
 
19
19
 
20
+ # Report JSON schema version (see docs/reference/report-schema.md).
21
+ # 1.0.1: findings gained optional account_id / account_name.
22
+ SCHEMA_VERSION = "1.0.1"
23
+
20
24
  # Maximum findings shown as individual rows in the Slack digest
21
25
  SLACK_DIGEST_LIMIT = 5
22
26
 
@@ -48,7 +52,7 @@ def build_report(
48
52
  total_waste = sum(f.estimated_monthly_cost for f in sorted_findings)
49
53
 
50
54
  return {
51
- "schema_version": "1.0",
55
+ "schema_version": SCHEMA_VERSION,
52
56
  "scan_id": str(uuid.uuid4()),
53
57
  "generated_at": datetime.now(tz=timezone.utc).isoformat(),
54
58
  "cloud": cloud,
@@ -11,6 +11,8 @@ import uuid
11
11
  from datetime import datetime, timezone
12
12
  from typing import Any
13
13
 
14
+ from core.reports.generator import SCHEMA_VERSION
15
+
14
16
  RESOURCE_TAXONOMY: dict[str, str] = {
15
17
  # Compute
16
18
  "AWS::EC2::Instance": "Compute Instance",
@@ -110,7 +112,7 @@ def merge_reports(reports: list[dict[str, Any]]) -> dict[str, Any]:
110
112
  total_waste = sum(f["estimated_monthly_cost"] for f in all_findings)
111
113
 
112
114
  return {
113
- "schema_version": "1.0",
115
+ "schema_version": SCHEMA_VERSION,
114
116
  "scan_id": str(uuid.uuid4()),
115
117
  "generated_at": datetime.now(tz=timezone.utc).isoformat(),
116
118
  "cloud": "multi",
@@ -128,7 +130,7 @@ def merge_reports(reports: list[dict[str, Any]]) -> dict[str, Any]:
128
130
 
129
131
  def _empty_merged_report() -> dict[str, Any]:
130
132
  return {
131
- "schema_version": "1.0",
133
+ "schema_version": SCHEMA_VERSION,
132
134
  "scan_id": str(uuid.uuid4()),
133
135
  "generated_at": datetime.now(tz=timezone.utc).isoformat(),
134
136
  "cloud": "multi",