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