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.
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/.env.example +27 -0
- {argus_cloud_optimizer-0.4.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.5.0}/PKG-INFO +89 -11
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/README.md +86 -9
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/adapter.py +3 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cloudwatch.py +10 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/monitor.py +10 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/base.py +5 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/adapter.py +5 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/asset_inventory.py +66 -27
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/cloud_logging.py +1 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/cloud_monitoring.py +12 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/azure_openai.py +16 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/vertexai.py +6 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0/argus_cloud_optimizer.egg-info}/PKG-INFO +89 -11
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/SOURCES.txt +22 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/requires.txt +1 -0
- argus_cloud_optimizer-0.5.0/core/__version__.py +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/chat.py +8 -4
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/loop.py +101 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/prompts.py +37 -4
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/config.py +1 -1
- argus_cloud_optimizer-0.5.0/core/registry/__init__.py +52 -0
- argus_cloud_optimizer-0.5.0/core/registry/aws.py +578 -0
- argus_cloud_optimizer-0.5.0/core/registry/azure.py +699 -0
- argus_cloud_optimizer-0.5.0/core/registry/factory.py +49 -0
- argus_cloud_optimizer-0.5.0/core/registry/gcp.py +759 -0
- argus_cloud_optimizer-0.5.0/core/registry/models.py +26 -0
- argus_cloud_optimizer-0.5.0/core/registry/registry.py +62 -0
- argus_cloud_optimizer-0.5.0/core/remediation/__init__.py +376 -0
- argus_cloud_optimizer-0.5.0/core/remediation/audit.py +57 -0
- argus_cloud_optimizer-0.5.0/core/remediation/engine.py +238 -0
- argus_cloud_optimizer-0.5.0/core/remediation/loader.py +289 -0
- argus_cloud_optimizer-0.5.0/core/remediation/models.py +126 -0
- argus_cloud_optimizer-0.5.0/core/remediation/rightsizing.py +164 -0
- argus_cloud_optimizer-0.5.0/core/remediation/validator.py +196 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/delivery.py +16 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/generator.py +48 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/html.py +31 -4
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/multi-account/spoke-role.yaml +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/single-account/template.yaml +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/gcp/deploy.sh +34 -2
- argus_cloud_optimizer-0.5.0/docs/blog/index.md +2 -0
- argus_cloud_optimizer-0.5.0/docs/blog/posts/react-vs-rule-based.md +107 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/ai-providers.md +25 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/configuration.md +2 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/quickstart.md +54 -2
- argus_cloud_optimizer-0.5.0/docs/index.md +441 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/env-vars.md +18 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/troubleshooting.md +25 -4
- argus_cloud_optimizer-0.5.0/docs/remediation/cli.md +174 -0
- argus_cloud_optimizer-0.5.0/docs/remediation/index.md +47 -0
- argus_cloud_optimizer-0.5.0/docs/remediation/jira.md +114 -0
- argus_cloud_optimizer-0.5.0/docs/remediation/policies.md +129 -0
- argus_cloud_optimizer-0.5.0/docs/roadmap.md +87 -0
- argus_cloud_optimizer-0.5.0/entrypoints/_remediation.py +73 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/aws_lambda.py +26 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/azure_function.py +27 -3
- argus_cloud_optimizer-0.5.0/entrypoints/cli.py +1150 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/cli_chat.py +2 -4
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/gcp_cloudrun.py +103 -17
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/pyproject.toml +2 -1
- argus_cloud_optimizer-0.4.0/core/__version__.py +0 -1
- argus_cloud_optimizer-0.4.0/docs/index.md +0 -278
- argus_cloud_optimizer-0.4.0/entrypoints/cli.py +0 -385
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/LICENSE +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/MANIFEST.in +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/accounts.yaml.example +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/auth.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cloudtrail.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/config.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/cost_explorer.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/resource_explorer.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/aws/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/activity_log.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/adapter.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/cost_management.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/resource_graph.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/azure/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/billing.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/adapters/gcp/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/anthropic.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/base.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/ai/bedrock.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/dependency_links.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/entry_points.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/argus_cloud_optimizer.egg-info/top_level.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/agent/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/log.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/models/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/models/finding.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/py.typed +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/comparison.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/export.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/reports/multi_cloud.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/secrets.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/token_tracker.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/core/validation.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/aws/multi-account/hub/template.yaml +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/deploy/azure/function-app.bicep +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/adapters.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/ai-reasoning.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/how-it-works.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/concepts/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/development.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/new-adapter.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/contributing/new-ai-provider.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/aws.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/azure.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/gcp.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/deployment/multi-account.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/first-scan.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/getting-started/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/iam-permissions.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/report-schema.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/resource-types.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/docs/reference/security.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.5.0}/entrypoints/__init__.py +0 -0
- {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
|
# -----------------------------------------------------------------------------
|
{argus_cloud_optimizer-0.4.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.5.0}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-cloud-optimizer
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: AI
|
|
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
|
|
66
|
+
<img src="docs/assets/images/logo.svg" alt="Argus" height="72">
|
|
66
67
|
</p>
|
|
67
68
|
|
|
68
|
-
<p align="center"><strong>
|
|
69
|
+
<p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
|
|
69
70
|
|
|
70
|
-
Argus
|
|
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
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
|
|
73
74
|
[](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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
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/ #
|
|
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
|
|
2
|
+
<img src="docs/assets/images/logo.svg" alt="Argus" height="72">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<p align="center"><strong>
|
|
5
|
+
<p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
|
|
6
6
|
|
|
7
|
-
Argus
|
|
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
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
|
|
10
10
|
[](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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
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/ #
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
"
|
|
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",
|