argus-cloud-optimizer 0.4.0__tar.gz → 0.5.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 (128) hide show
  1. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/.env.example +27 -0
  2. {argus_cloud_optimizer-0.4.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.5.0}/PKG-INFO +89 -11
  3. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/README.md +86 -9
  4. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/adapter.py +3 -1
  5. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cloudwatch.py +10 -3
  6. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/monitor.py +10 -1
  7. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/base.py +5 -1
  8. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/adapter.py +5 -1
  9. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/asset_inventory.py +66 -27
  10. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/cloud_logging.py +1 -2
  11. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/cloud_monitoring.py +12 -3
  12. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/azure_openai.py +16 -0
  13. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/vertexai.py +6 -3
  14. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0/argus_cloud_optimizer.egg-info}/PKG-INFO +89 -11
  15. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/SOURCES.txt +22 -0
  16. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/requires.txt +1 -0
  17. argus_cloud_optimizer-0.5.0/core/__version__.py +1 -0
  18. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/chat.py +8 -4
  19. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/loop.py +101 -0
  20. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/prompts.py +37 -4
  21. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/config.py +1 -1
  22. argus_cloud_optimizer-0.5.0/core/registry/__init__.py +52 -0
  23. argus_cloud_optimizer-0.5.0/core/registry/aws.py +578 -0
  24. argus_cloud_optimizer-0.5.0/core/registry/azure.py +699 -0
  25. argus_cloud_optimizer-0.5.0/core/registry/factory.py +49 -0
  26. argus_cloud_optimizer-0.5.0/core/registry/gcp.py +759 -0
  27. argus_cloud_optimizer-0.5.0/core/registry/models.py +26 -0
  28. argus_cloud_optimizer-0.5.0/core/registry/registry.py +62 -0
  29. argus_cloud_optimizer-0.5.0/core/remediation/__init__.py +376 -0
  30. argus_cloud_optimizer-0.5.0/core/remediation/audit.py +57 -0
  31. argus_cloud_optimizer-0.5.0/core/remediation/engine.py +238 -0
  32. argus_cloud_optimizer-0.5.0/core/remediation/loader.py +289 -0
  33. argus_cloud_optimizer-0.5.0/core/remediation/models.py +126 -0
  34. argus_cloud_optimizer-0.5.0/core/remediation/rightsizing.py +164 -0
  35. argus_cloud_optimizer-0.5.0/core/remediation/validator.py +196 -0
  36. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/delivery.py +16 -2
  37. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/generator.py +48 -2
  38. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/html.py +31 -4
  39. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/multi-account/spoke-role.yaml +1 -0
  40. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/single-account/template.yaml +1 -0
  41. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/gcp/deploy.sh +34 -2
  42. argus_cloud_optimizer-0.5.0/docs/blog/index.md +2 -0
  43. argus_cloud_optimizer-0.5.0/docs/blog/posts/react-vs-rule-based.md +107 -0
  44. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/ai-providers.md +25 -3
  45. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/configuration.md +2 -2
  46. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/quickstart.md +54 -2
  47. argus_cloud_optimizer-0.5.0/docs/index.md +441 -0
  48. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/env-vars.md +18 -3
  49. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/troubleshooting.md +25 -4
  50. argus_cloud_optimizer-0.5.0/docs/remediation/cli.md +174 -0
  51. argus_cloud_optimizer-0.5.0/docs/remediation/index.md +47 -0
  52. argus_cloud_optimizer-0.5.0/docs/remediation/jira.md +114 -0
  53. argus_cloud_optimizer-0.5.0/docs/remediation/policies.md +129 -0
  54. argus_cloud_optimizer-0.5.0/docs/roadmap.md +87 -0
  55. argus_cloud_optimizer-0.5.0/entrypoints/_remediation.py +73 -0
  56. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/aws_lambda.py +26 -3
  57. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/azure_function.py +27 -3
  58. argus_cloud_optimizer-0.5.0/entrypoints/cli.py +1150 -0
  59. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/cli_chat.py +2 -4
  60. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/gcp_cloudrun.py +103 -17
  61. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/pyproject.toml +2 -1
  62. argus_cloud_optimizer-0.4.0/core/__version__.py +0 -1
  63. argus_cloud_optimizer-0.4.0/docs/index.md +0 -278
  64. argus_cloud_optimizer-0.4.0/entrypoints/cli.py +0 -385
  65. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/LICENSE +0 -0
  66. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/MANIFEST.in +0 -0
  67. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/accounts.yaml.example +0 -0
  68. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/__init__.py +0 -0
  69. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/__init__.py +0 -0
  70. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/auth.py +0 -0
  71. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cloudtrail.py +0 -0
  72. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/config.py +0 -0
  73. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cost_explorer.py +0 -0
  74. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/resource_explorer.py +0 -0
  75. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/retry.py +0 -0
  76. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/__init__.py +0 -0
  77. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/activity_log.py +0 -0
  78. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/adapter.py +0 -0
  79. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/cost_management.py +0 -0
  80. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/resource_graph.py +0 -0
  81. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/retry.py +0 -0
  82. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/__init__.py +0 -0
  83. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/billing.py +0 -0
  84. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/retry.py +0 -0
  85. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/__init__.py +0 -0
  86. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/anthropic.py +0 -0
  87. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/base.py +0 -0
  88. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/bedrock.py +0 -0
  89. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/dependency_links.txt +0 -0
  90. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/entry_points.txt +0 -0
  91. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/top_level.txt +0 -0
  92. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/__init__.py +0 -0
  93. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/__init__.py +0 -0
  94. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/log.py +0 -0
  95. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/models/__init__.py +0 -0
  96. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/models/finding.py +0 -0
  97. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/py.typed +0 -0
  98. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/__init__.py +0 -0
  99. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/comparison.py +0 -0
  100. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/export.py +0 -0
  101. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/multi_cloud.py +0 -0
  102. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/secrets.py +0 -0
  103. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/token_tracker.py +0 -0
  104. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/validation.py +0 -0
  105. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/multi-account/hub/template.yaml +0 -0
  106. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/azure/function-app.bicep +0 -0
  107. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/adapters.md +0 -0
  108. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/ai-reasoning.md +0 -0
  109. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/how-it-works.md +0 -0
  110. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/index.md +0 -0
  111. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/development.md +0 -0
  112. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/index.md +0 -0
  113. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/new-adapter.md +0 -0
  114. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/new-ai-provider.md +0 -0
  115. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/aws.md +0 -0
  116. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/azure.md +0 -0
  117. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/gcp.md +0 -0
  118. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/index.md +0 -0
  119. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/multi-account.md +0 -0
  120. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/first-scan.md +0 -0
  121. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/index.md +0 -0
  122. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/iam-permissions.md +0 -0
  123. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/index.md +0 -0
  124. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/report-schema.md +0 -0
  125. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/resource-types.md +0 -0
  126. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/security.md +0 -0
  127. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/__init__.py +0 -0
  128. {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/setup.cfg +0 -0
@@ -68,6 +68,33 @@ REPORT_FORMAT=json,html # comma-separated: json, html, pdf,
68
68
  # Useful for local testing without a real webhook.
69
69
  DRY_RUN=false
70
70
 
71
+ # -----------------------------------------------------------------------------
72
+ # Remediation — Policy Engine + Jira Integration
73
+ # Policies are YAML files in ARGUS_POLICIES_DIR. Argus never executes changes;
74
+ # it creates Jira tickets for human review. See config/policies/ for examples
75
+ # and CONTRIBUTING.md for how to write policies.
76
+ # -----------------------------------------------------------------------------
77
+ ARGUS_POLICIES_DIR=./config/policies # directory containing policy YAML files
78
+
79
+ # Jira credentials — required for `argus policies apply --confirm`
80
+ # Create an API token at: https://id.atlassian.com/manage-profile/security/api-tokens
81
+ JIRA_BASE_URL=https://your-org.atlassian.net
82
+ JIRA_USER_EMAIL=bot@your-org.com
83
+ JIRA_API_TOKEN= # Atlassian API token (not your password)
84
+
85
+ # Directory containing your *.yaml policy files (one policy per file).
86
+ # Copy from config/policies.example/ to get started, then customise.
87
+ ARGUS_POLICY_DIR=./config/policies
88
+
89
+ # Path to integrations config (project key, labels, priority map).
90
+ # Copy config/integrations.yaml.example → config/integrations.yaml and fill in.
91
+ # config/integrations.yaml is gitignored — never commit credentials.
92
+ ARGUS_INTEGRATIONS_CONFIG=./config/integrations.yaml
93
+
94
+ # Audit log — every `apply --confirm` appends one JSONL line mapping
95
+ # proposal_id → jira_key → jira_url for traceability.
96
+ ARGUS_AUDIT_LOG=./local_reports/audit.jsonl
97
+
71
98
  # -----------------------------------------------------------------------------
72
99
  # Logging
73
100
  # -----------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-cloud-optimizer
3
- Version: 0.4.0
4
- Summary: AI-powered multi-cloud cost optimization agent
3
+ Version: 0.5.0
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
7
7
  Project-URL: Homepage, https://github.com/vamshisiddarth/argus
@@ -59,15 +59,16 @@ Requires-Dist: types-python-dateutil>=2.9.0; extra == "dev"
59
59
  Requires-Dist: boto3-stubs[bedrock-runtime,ce,cloudwatch,resourceexplorer2,s3,sts]>=1.35.36; extra == "dev"
60
60
  Requires-Dist: mkdocs-material>=9.5.44; extra == "dev"
61
61
  Requires-Dist: mkdocs-minify-plugin>=0.8.0; extra == "dev"
62
+ Requires-Dist: mike>=2.1.0; extra == "dev"
62
63
  Dynamic: license-file
63
64
 
64
65
  <p align="center">
65
- <img src="docs/assets/images/logo-full.svg" alt="Argus" height="72">
66
+ <img src="docs/assets/images/logo.svg" alt="Argus" height="72">
66
67
  </p>
67
68
 
68
- <p align="center"><strong>AI-powered cloud cost optimization agent for AWS, GCP, and Azure.</strong></p>
69
+ <p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
69
70
 
70
- Argus finds idle and wasted cloud resources — stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases — and delivers a prioritized, AI-reasoned report to Slack every week.
71
+ Argus is an open-source AI Cloud Detective — it hunts idle and wasted cloud resources across AWS, GCP, and Azure (stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases) and delivers a prioritized, AI-reasoned report to Slack every week. No rules. No thresholds. Just reasoning.
71
72
 
72
73
  [![CI](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml/badge.svg)](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
73
74
  [![Publish](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml/badge.svg)](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml)
@@ -100,6 +101,21 @@ Top findings
100
101
 
101
102
  ---
102
103
 
104
+ ## 🆕 What's new in v0.5.0
105
+
106
+ > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
107
+
108
+ - **Policy engine** — YAML-driven policies match AI findings and generate prioritised `ChangeProposal` objects (weight-ordered, one proposal per resource per scan). Ships with 13 real-world policies across AWS, GCP, and Azure.
109
+ - **Jira integration** — idempotent ticket creation with ADF-structured descriptions (metrics table, runbook code block, snapshot fingerprint for diff-on-update). Dedup via deterministic label; diff-comment on re-scan.
110
+ - **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%").
111
+ - **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.
112
+ - **JSONL audit log** — every `apply --confirm` appends a line to `local_reports/audit.jsonl` mapping `proposal_id → jira_key → jira_url`.
113
+ - **1756 tests** — all pass offline, no cloud credentials needed.
114
+
115
+ [Full changelog →](CHANGELOG.md)
116
+
117
+ ---
118
+
103
119
  ## What it does
104
120
 
105
121
  Every week (or on demand), Argus:
@@ -115,6 +131,63 @@ The **Full report** button links to a self-contained HTML file (S3 / GCS / Azure
115
131
 
116
132
  ---
117
133
 
134
+ ## Remediation
135
+
136
+ Argus stays **strictly read-only** — it never mutates a resource. Instead, the policy engine turns AI findings into prioritised Jira tickets that a human reviews and acts on.
137
+
138
+ ### How it works
139
+
140
+ ```
141
+ argus scan → scan_report.json
142
+ │
143
+ ▼
144
+ argus policies plan --report scan_report.json
145
+ │
146
+ ├── evaluates policies in config/policies/ (13 bundled — weight-ordered, first-match-wins)
147
+ ├── applies two-tier conditions: cost/priority/idle-days + metric thresholds
148
+ ├── computes rightsizing target (e.g. "Recommend db.t3.small — CPU ~8%")
149
+ └── prints proposal table with total savings estimate
150
+ │
151
+ ▼ (add --confirm to create tickets)
152
+ argus policies apply --report scan_report.json --confirm
153
+ │
154
+ ├── creates Jira ticket per proposal (idempotent — dedup via label)
155
+ ├── diff-comments on re-scan if finding changed
156
+ ├── appends line to local_reports/audit.jsonl
157
+ └── prints per-ticket status
158
+
159
+ argus policies stats # acceptance rate per policy from audit log
160
+ ```
161
+
162
+ ### Bundled policies (13)
163
+
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).
181
+
182
+ ### Safety guarantees
183
+
184
+ - **IAM** — read-only roles; write APIs are unavailable even with creds attached
185
+ - **Code** — no cloud SDK write calls anywhere in the codebase
186
+ - **CLI gate** — `apply` is a dry-run by default; `--confirm` is required to create tickets
187
+ - **Human gate** — runbook is printed in the Jira ticket; Argus never executes it
188
+
189
+ ---
190
+
118
191
  ## Architecture
119
192
 
120
193
  ```
@@ -166,13 +239,13 @@ docker run --rm \
166
239
 
167
240
  ```bash
168
241
  pip install argus-cloud-optimizer
169
- argus --version # argus 0.3.0
242
+ argus --version # argus 0.5.0
170
243
  argus --help
171
244
  ```
172
245
 
173
246
  One package — all three clouds included. No extras needed. `--cloud` auto-detects from your environment (GCP_PROJECT_ID / AZURE_SUBSCRIPTION_IDS / AWS credentials), or specify explicitly.
174
247
 
175
- > **Verified 2026-06-21:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
248
+ > **Verified 2026-06-24:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
176
249
 
177
250
  **AWS:**
178
251
 
@@ -231,7 +304,7 @@ argus chat # auto-detects cloud
231
304
  ```
232
305
 
233
306
  ```
234
- Argus v0.3.0 — Interactive Cloud Cost Assistant
307
+ Argus v0.5.0 — AI Cloud Detective
235
308
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
236
309
  Tip: end a line with \ to continue on the next line.
237
310
  Type your question, or /help for commands.
@@ -531,7 +604,7 @@ Before you invest time deploying Argus, know what it **can't** do yet:
531
604
  | **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. |
532
605
  | **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`. |
533
606
  | **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. |
534
- | **Write actions** | None | Argus is read-only. It reports findings but never deletes, stops, or modifies resources. Remediation is manual. |
607
+ | **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. |
535
608
  | **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. |
536
609
  | **Notifications** | Slack + Teams + webhook | No email. Slack/Teams delivery requires a webhook URL. |
537
610
 
@@ -573,6 +646,7 @@ argus/
573
646
  │ ├── agent/loop.py # ReAct agent loop
574
647
  │ ├── agent/prompts.py # System prompt + tool schemas
575
648
  │ ├── models/finding.py # ResourceFinding dataclass
649
+ │ ├── remediation/ # Policy engine — models, loader, validator, engine, audit, rightsizing
576
650
  │ └── reports/ # Report generator, multi-cloud merge, export, notifications
577
651
  ├── adapters/
578
652
  │ ├── base.py # CloudAdapter abstract class
@@ -585,8 +659,12 @@ argus/
585
659
  │ ├── bedrock.py # AWS Bedrock (Converse API)
586
660
  │ ├── vertexai.py # Vertex AI / Gemini (GCP)
587
661
  │ └── azure_openai.py # Azure OpenAI / GPT-4o (Azure)
662
+ ├── integrations/
663
+ │ └── jira/ # Ticket lifecycle: create, dedup, diff-comment, ADF formatter, webhook
664
+ ├── config/policies/ # 13 bundled YAML policies (AWS / GCP / Azure)
665
+ ├── config/policies.example/ # 3 annotated starter templates — copy here to begin
588
666
  ├── entrypoints/
589
- │ ├── cli.py # argus --cloud aws --run-now
667
+ │ ├── cli.py # argus scan / chat / policies validate|plan|apply|stats|docs
590
668
  │ ├── aws_lambda.py # AWS Lambda handler
591
669
  │ ├── gcp_cloudrun.py # GCP Cloud Run Job handler
592
670
  │ └── azure_function.py # Azure Function timer trigger
@@ -594,7 +672,7 @@ argus/
594
672
  │ ├── aws/ # CloudFormation templates
595
673
  │ ├── gcp/ # Cloud Run + Scheduler deploy script
596
674
  │ └── azure/ # Bicep templates
597
- └── tests/ # 560 tests, all pass offline
675
+ └── tests/ # 1756 tests, all pass offline
598
676
  ```
599
677
 
600
678
  ---
@@ -1,10 +1,10 @@
1
1
  <p align="center">
2
- <img src="docs/assets/images/logo-full.svg" alt="Argus" height="72">
2
+ <img src="docs/assets/images/logo.svg" alt="Argus" height="72">
3
3
  </p>
4
4
 
5
- <p align="center"><strong>AI-powered cloud cost optimization agent for AWS, GCP, and Azure.</strong></p>
5
+ <p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
6
6
 
7
- Argus finds idle and wasted cloud resources — stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases — and delivers a prioritized, AI-reasoned report to Slack every week.
7
+ Argus is an open-source AI Cloud Detective — it hunts idle and wasted cloud resources across AWS, GCP, and Azure (stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases) and delivers a prioritized, AI-reasoned report to Slack every week. No rules. No thresholds. Just reasoning.
8
8
 
9
9
  [![CI](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml/badge.svg)](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
10
10
  [![Publish](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml/badge.svg)](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml)
@@ -37,6 +37,21 @@ Top findings
37
37
 
38
38
  ---
39
39
 
40
+ ## 🆕 What's new in v0.5.0
41
+
42
+ > **Remediation is now first-class.** Argus goes beyond reporting — it generates Jira tickets, suggests exact resize targets, and tracks acceptance rates.
43
+
44
+ - **Policy engine** — YAML-driven policies match AI findings and generate prioritised `ChangeProposal` objects (weight-ordered, one proposal per resource per scan). Ships with 13 real-world policies across AWS, GCP, and Azure.
45
+ - **Jira integration** — idempotent ticket creation with ADF-structured descriptions (metrics table, runbook code block, snapshot fingerprint for diff-on-update). Dedup via deterministic label; diff-comment on re-scan.
46
+ - **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%").
47
+ - **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.
48
+ - **JSONL audit log** — every `apply --confirm` appends a line to `local_reports/audit.jsonl` mapping `proposal_id → jira_key → jira_url`.
49
+ - **1756 tests** — all pass offline, no cloud credentials needed.
50
+
51
+ [Full changelog →](CHANGELOG.md)
52
+
53
+ ---
54
+
40
55
  ## What it does
41
56
 
42
57
  Every week (or on demand), Argus:
@@ -52,6 +67,63 @@ The **Full report** button links to a self-contained HTML file (S3 / GCS / Azure
52
67
 
53
68
  ---
54
69
 
70
+ ## Remediation
71
+
72
+ Argus stays **strictly read-only** — it never mutates a resource. Instead, the policy engine turns AI findings into prioritised Jira tickets that a human reviews and acts on.
73
+
74
+ ### How it works
75
+
76
+ ```
77
+ argus scan → scan_report.json
78
+ │
79
+ ▼
80
+ argus policies plan --report scan_report.json
81
+ │
82
+ ├── evaluates policies in config/policies/ (13 bundled — weight-ordered, first-match-wins)
83
+ ├── applies two-tier conditions: cost/priority/idle-days + metric thresholds
84
+ ├── computes rightsizing target (e.g. "Recommend db.t3.small — CPU ~8%")
85
+ └── prints proposal table with total savings estimate
86
+ │
87
+ ▼ (add --confirm to create tickets)
88
+ argus policies apply --report scan_report.json --confirm
89
+ │
90
+ ├── creates Jira ticket per proposal (idempotent — dedup via label)
91
+ ├── diff-comments on re-scan if finding changed
92
+ ├── appends line to local_reports/audit.jsonl
93
+ └── prints per-ticket status
94
+
95
+ argus policies stats # acceptance rate per policy from audit log
96
+ ```
97
+
98
+ ### Bundled policies (13)
99
+
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).
117
+
118
+ ### Safety guarantees
119
+
120
+ - **IAM** — read-only roles; write APIs are unavailable even with creds attached
121
+ - **Code** — no cloud SDK write calls anywhere in the codebase
122
+ - **CLI gate** — `apply` is a dry-run by default; `--confirm` is required to create tickets
123
+ - **Human gate** — runbook is printed in the Jira ticket; Argus never executes it
124
+
125
+ ---
126
+
55
127
  ## Architecture
56
128
 
57
129
  ```
@@ -103,13 +175,13 @@ docker run --rm \
103
175
 
104
176
  ```bash
105
177
  pip install argus-cloud-optimizer
106
- argus --version # argus 0.3.0
178
+ argus --version # argus 0.5.0
107
179
  argus --help
108
180
  ```
109
181
 
110
182
  One package — all three clouds included. No extras needed. `--cloud` auto-detects from your environment (GCP_PROJECT_ID / AZURE_SUBSCRIPTION_IDS / AWS credentials), or specify explicitly.
111
183
 
112
- > **Verified 2026-06-21:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
184
+ > **Verified 2026-06-24:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
113
185
 
114
186
  **AWS:**
115
187
 
@@ -168,7 +240,7 @@ argus chat # auto-detects cloud
168
240
  ```
169
241
 
170
242
  ```
171
- Argus v0.3.0 — Interactive Cloud Cost Assistant
243
+ Argus v0.5.0 — AI Cloud Detective
172
244
  Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
173
245
  Tip: end a line with \ to continue on the next line.
174
246
  Type your question, or /help for commands.
@@ -468,7 +540,7 @@ Before you invest time deploying Argus, know what it **can't** do yet:
468
540
  | **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. |
469
541
  | **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`. |
470
542
  | **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. |
471
- | **Write actions** | None | Argus is read-only. It reports findings but never deletes, stops, or modifies resources. Remediation is manual. |
543
+ | **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. |
472
544
  | **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. |
473
545
  | **Notifications** | Slack + Teams + webhook | No email. Slack/Teams delivery requires a webhook URL. |
474
546
 
@@ -510,6 +582,7 @@ argus/
510
582
  │ ├── agent/loop.py # ReAct agent loop
511
583
  │ ├── agent/prompts.py # System prompt + tool schemas
512
584
  │ ├── models/finding.py # ResourceFinding dataclass
585
+ │ ├── remediation/ # Policy engine — models, loader, validator, engine, audit, rightsizing
513
586
  │ └── reports/ # Report generator, multi-cloud merge, export, notifications
514
587
  ├── adapters/
515
588
  │ ├── base.py # CloudAdapter abstract class
@@ -522,8 +595,12 @@ argus/
522
595
  │ ├── bedrock.py # AWS Bedrock (Converse API)
523
596
  │ ├── vertexai.py # Vertex AI / Gemini (GCP)
524
597
  │ └── azure_openai.py # Azure OpenAI / GPT-4o (Azure)
598
+ ├── integrations/
599
+ │ └── jira/ # Ticket lifecycle: create, dedup, diff-comment, ADF formatter, webhook
600
+ ├── config/policies/ # 13 bundled YAML policies (AWS / GCP / Azure)
601
+ ├── config/policies.example/ # 3 annotated starter templates — copy here to begin
525
602
  ├── entrypoints/
526
- │ ├── cli.py # argus --cloud aws --run-now
603
+ │ ├── cli.py # argus scan / chat / policies validate|plan|apply|stats|docs
527
604
  │ ├── aws_lambda.py # AWS Lambda handler
528
605
  │ ├── gcp_cloudrun.py # GCP Cloud Run Job handler
529
606
  │ └── azure_function.py # Azure Function timer trigger
@@ -531,7 +608,7 @@ argus/
531
608
  │ ├── aws/ # CloudFormation templates
532
609
  │ ├── gcp/ # Cloud Run + Scheduler deploy script
533
610
  │ └── azure/ # Bicep templates
534
- └── tests/ # 560 tests, all pass offline
611
+ └── tests/ # 1756 tests, all pass offline
535
612
  ```
536
613
 
537
614
  ---
@@ -82,4 +82,6 @@ class AWSAdapter(CloudAdapter):
82
82
  ) -> "AWSAdapter":
83
83
  """Convenience constructor — resolves auth and returns a ready adapter."""
84
84
  session = auth.get_session(account=account, region=region)
85
- return cls(session=session, aggregator_region=region)
85
+ # Pass aggregator_region=None so __init__ honours RESOURCE_EXPLORER_REGION
86
+ # env var before falling back to the default "us-east-1".
87
+ return cls(session=session, aggregator_region=None)
@@ -11,11 +11,18 @@ from botocore.exceptions import ClientError
11
11
  from adapters.aws.config import BOTO_TIMEOUT_CONFIG
12
12
  from adapters.aws.retry import retry_on_transient
13
13
  from adapters.base import MetricSummary
14
+ from core.registry import get_registry
14
15
 
15
16
  logger = structlog.get_logger(__name__)
16
17
 
17
- # (MetricName, Namespace, Stat, CloudWatch Dimension Key)
18
- # Stat is "Average" for utilisation metrics, "Sum" for throughput/count metrics.
18
+
19
+ def _metrics_for(resource_type: str) -> list[tuple[str, str, str, str]] | None:
20
+ spec = get_registry().get(resource_type)
21
+ if spec and spec.metrics:
22
+ return [(m.name, m.namespace, m.stat, m.dimension_key) for m in spec.metrics]
23
+ return None
24
+
25
+
19
26
  _METRICS: dict[str, list[tuple[str, str, str, str]]] = {
20
27
  "AWS::EC2::Instance": [
21
28
  ("CPUUtilization", "AWS/EC2", "Average", "InstanceId"),
@@ -282,7 +289,7 @@ def get_metrics(
282
289
  gives the AI the current instance size so it can recommend a specific
283
290
  smaller tier rather than a generic "consider downsizing".
284
291
  """
285
- metric_defs = _METRICS.get(resource_type)
292
+ metric_defs = _metrics_for(resource_type)
286
293
  if not metric_defs:
287
294
  metric_defs = _discover_metrics(session, resource_id, resource_type)
288
295
  if not metric_defs:
@@ -10,9 +10,18 @@ from azure.monitor.query import MetricAggregationType, MetricsQueryClient
10
10
 
11
11
  from adapters.azure.retry import retry_on_transient
12
12
  from adapters.base import MetricSummary
13
+ from core.registry import get_registry
13
14
 
14
15
  logger = structlog.get_logger(__name__)
15
16
 
17
+
18
+ def _metrics_for(resource_type: str) -> list[tuple[str, str]] | None:
19
+ spec = get_registry().get(resource_type.lower())
20
+ if spec and spec.metrics:
21
+ return [(m.name, m.stat) for m in spec.metrics]
22
+ return None
23
+
24
+
16
25
  # (MetricName, AggregationType)
17
26
  _METRICS: dict[str, list[tuple[str, str]]] = {
18
27
  # Virtual Machines
@@ -271,7 +280,7 @@ def get_metrics(
271
280
  Fetch Azure Monitor metrics for a resource.
272
281
  Falls back to querying available metric definitions for unknown resource types.
273
282
  """
274
- metric_defs = _METRICS.get(resource_type.lower())
283
+ metric_defs = _metrics_for(resource_type)
275
284
  if not metric_defs:
276
285
  metric_defs = _discover_metrics(resource_id, resource_type, credential)
277
286
  if not metric_defs:
@@ -52,7 +52,11 @@ class CloudAdapter(ABC):
52
52
  """
53
53
  Abstract cloud adapter. One implementation per cloud provider.
54
54
  The agent loop only ever calls these four methods — never raw SDK clients.
55
- All implementations must be read-only (no mutations to cloud resources).
55
+
56
+ READ-ONLY CONTRACT: All implementations must be strictly read-only.
57
+ No method may create, modify, delete, or mutate any cloud resource.
58
+ This contract is enforced by tests that scan adapter subclasses for
59
+ mutating method names. Violations fail CI.
56
60
  """
57
61
 
58
62
  @abstractmethod
@@ -33,12 +33,16 @@ class GCPAdapter(CloudAdapter):
33
33
  )
34
34
  self._project_id: str = resolved
35
35
  self._bq_billing_table = bq_billing_table or os.environ.get("BILLING_BQ_TABLE")
36
+ # Populated after list_resources() — asset types skipped due to disabled APIs
37
+ self.skipped_asset_types: list[str] = []
36
38
 
37
39
  def list_resources(self, ignore_regions: list[str] | None = None) -> list[Resource]:
38
- return asset_inventory.list_resources(
40
+ resources, skipped = asset_inventory.list_resources(
39
41
  project_id=self._project_id,
40
42
  ignore_regions=ignore_regions,
41
43
  )
44
+ self.skipped_asset_types = skipped
45
+ return resources
42
46
 
43
47
  def get_metrics(
44
48
  self,
@@ -3,7 +3,12 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  import structlog
6
- from google.api_core.exceptions import GoogleAPICallError, PermissionDenied
6
+ from google.api_core.exceptions import (
7
+ GoogleAPICallError,
8
+ InvalidArgument,
9
+ NotFound,
10
+ PermissionDenied,
11
+ )
7
12
  from google.cloud import asset_v1
8
13
 
9
14
  from adapters.base import Resource
@@ -32,7 +37,7 @@ SCANNED_ASSET_TYPES: list[str] = [
32
37
  "compute.googleapis.com/VpnTunnel",
33
38
  "vpcaccess.googleapis.com/Connector", # Serverless VPC
34
39
  # --- Databases ---
35
- "sql.googleapis.com/Instance", # Cloud SQL
40
+ "sqladmin.googleapis.com/Instance", # Cloud SQL
36
41
  "spanner.googleapis.com/Instance",
37
42
  "bigtable.googleapis.com/Instance",
38
43
  "alloydb.googleapis.com/Cluster", # AlloyDB (managed Postgres)
@@ -55,42 +60,76 @@ SCANNED_ASSET_TYPES: list[str] = [
55
60
  def list_resources(
56
61
  project_id: str,
57
62
  ignore_regions: list[str] | None = None,
58
- ) -> list[Resource]:
63
+ ) -> tuple[list[Resource], list[str]]:
59
64
  """
60
- Return all billable GCP resources in a project using Cloud Asset Inventory.
61
- Uses a single paginated API call — no per-resource-type enumeration needed.
65
+ Return (resources, skipped_asset_types) for a GCP project.
66
+
67
+ Uses a single paginated Cloud Asset Inventory call. If any asset type's API
68
+ is not enabled in the project, INVALID_ARGUMENT is returned for that type.
69
+ We strip it and retry so a project without Bigtable/Spanner/etc. enabled
70
+ doesn't block the whole scan. Callers receive the list of skipped types so
71
+ they can surface a warning in the Slack digest.
62
72
  """
63
73
  client = asset_v1.AssetServiceClient()
64
74
  parent = f"projects/{project_id}"
65
75
  ignore_set = set(ignore_regions or [])
66
76
  resources: list[Resource] = []
67
-
68
- request = asset_v1.ListAssetsRequest(
69
- parent=parent,
70
- asset_types=SCANNED_ASSET_TYPES,
71
- content_type=asset_v1.ContentType.RESOURCE,
72
- )
73
-
74
- try:
75
- for asset in retry_on_transient(
76
- client.list_assets, request=request, timeout=60
77
- ):
78
- parsed = _parse_asset(asset, ignore_set)
79
- if parsed:
80
- resources.append(parsed)
81
- except PermissionDenied as exc:
82
- raise PermissionError(
83
- f"Argus service account is missing cloudasset.assets.listAssets "
84
- f"permission on project {project_id}."
85
- ) from exc
86
- except GoogleAPICallError as exc:
87
- raise RuntimeError(f"Cloud Asset Inventory API error: {exc}") from exc
77
+ asset_types = list(SCANNED_ASSET_TYPES) # mutable — bad types get stripped
78
+ skipped_types: list[str] = []
79
+
80
+ while True:
81
+ request = asset_v1.ListAssetsRequest(
82
+ parent=parent,
83
+ asset_types=asset_types,
84
+ content_type=asset_v1.ContentType.RESOURCE,
85
+ )
86
+ try:
87
+ for asset in retry_on_transient(
88
+ client.list_assets, request=request, timeout=60
89
+ ):
90
+ parsed = _parse_asset(asset, ignore_set)
91
+ if parsed:
92
+ resources.append(parsed)
93
+ break # success — exit retry loop
94
+ except InvalidArgument as exc:
95
+ # An asset type whose API is not enabled in this project causes
96
+ # INVALID_ARGUMENT. Strip it and retry with the remaining types.
97
+ bad_type = _extract_bad_asset_type(str(exc), asset_types)
98
+ if bad_type:
99
+ logger.warning(
100
+ "asset_type_api_not_enabled_skipping",
101
+ asset_type=bad_type,
102
+ project_id=project_id,
103
+ )
104
+ skipped_types.append(bad_type)
105
+ asset_types.remove(bad_type)
106
+ resources = [] # reset — avoid duplicates from partial page
107
+ else:
108
+ raise RuntimeError(
109
+ f"Cloud Asset Inventory INVALID_ARGUMENT (unknown type): {exc}"
110
+ ) from exc
111
+ except (PermissionDenied, NotFound) as exc:
112
+ raise PermissionError(
113
+ f"Project '{project_id}' not found or Argus lacks permission. "
114
+ f"Check the project ID and ensure cloudasset.assets.listAssets "
115
+ f"is granted to your credentials."
116
+ ) from exc
117
+ except GoogleAPICallError as exc:
118
+ raise RuntimeError(f"Cloud Asset Inventory API error: {exc}") from exc
88
119
 
89
120
  logger.info(
90
121
  "asset_inventory_complete",
91
122
  extra={"project_id": project_id, "total": len(resources)},
92
123
  )
93
- return resources
124
+ return resources, skipped_types
125
+
126
+
127
+ def _extract_bad_asset_type(error_msg: str, asset_types: list[str]) -> str | None:
128
+ """Return the first asset type string found in an INVALID_ARGUMENT error."""
129
+ for asset_type in asset_types:
130
+ if asset_type in error_msg:
131
+ return asset_type
132
+ return None
94
133
 
95
134
 
96
135
  def _parse_asset(asset: Any, ignore_set: set[str]) -> Resource | None:
@@ -52,7 +52,6 @@ def get_last_activity(
52
52
  filter_=log_filter,
53
53
  order_by=gcp_logging.DESCENDING,
54
54
  page_size=1,
55
- timeout=60,
56
55
  )
57
56
  )
58
57
  except GoogleAPICallError as exc:
@@ -76,7 +75,7 @@ def _service_from_resource_type(resource_type: str) -> str | None:
76
75
  mapping: dict[str, str] = {
77
76
  "compute.googleapis.com/Instance": "compute.googleapis.com",
78
77
  "compute.googleapis.com/Disk": "compute.googleapis.com",
79
- "sql.googleapis.com/Instance": "cloudsql.googleapis.com",
78
+ "sqladmin.googleapis.com/Instance": "cloudsql.googleapis.com",
80
79
  "container.googleapis.com/Cluster": "container.googleapis.com",
81
80
  "storage.googleapis.com/Bucket": "storage.googleapis.com",
82
81
  "bigquery.googleapis.com/Dataset": "bigquery.googleapis.com",