cloud-cost-cli 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/bin/cloud-cost-cli.d.ts +2 -0
- package/dist/bin/cloud-cost-cli.js +23 -0
- package/dist/src/analyzers/cost-estimator.d.ts +21 -0
- package/dist/src/analyzers/cost-estimator.js +86 -0
- package/dist/src/commands/scan.d.ts +12 -0
- package/dist/src/commands/scan.js +111 -0
- package/dist/src/providers/aws/client.d.ts +27 -0
- package/dist/src/providers/aws/client.js +53 -0
- package/dist/src/providers/aws/ebs.d.ts +3 -0
- package/dist/src/providers/aws/ebs.js +41 -0
- package/dist/src/providers/aws/ec2.d.ts +3 -0
- package/dist/src/providers/aws/ec2.js +75 -0
- package/dist/src/providers/aws/eip.d.ts +3 -0
- package/dist/src/providers/aws/eip.js +38 -0
- package/dist/src/providers/aws/elb.d.ts +3 -0
- package/dist/src/providers/aws/elb.js +66 -0
- package/dist/src/providers/aws/rds.d.ts +3 -0
- package/dist/src/providers/aws/rds.js +92 -0
- package/dist/src/providers/aws/s3.d.ts +3 -0
- package/dist/src/providers/aws/s3.js +54 -0
- package/dist/src/reporters/json.d.ts +2 -0
- package/dist/src/reporters/json.js +6 -0
- package/dist/src/reporters/table.d.ts +2 -0
- package/dist/src/reporters/table.js +41 -0
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.js +18 -0
- package/dist/src/types/opportunity.d.ts +31 -0
- package/dist/src/types/opportunity.js +2 -0
- package/dist/src/types/provider.d.ts +15 -0
- package/dist/src/types/provider.js +2 -0
- package/dist/src/utils/formatter.d.ts +3 -0
- package/dist/src/utils/formatter.js +16 -0
- package/dist/src/utils/index.d.ts +2 -0
- package/dist/src/utils/index.js +18 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +30 -0
- package/docs/RELEASE.md +143 -0
- package/docs/contributing.md +201 -0
- package/docs/iam-policy.json +25 -0
- package/docs/installation.md +208 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Phuong Vu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# cloud-cost-cli
|
|
2
|
+
|
|
3
|
+
**Optimize your cloud spend in seconds.**
|
|
4
|
+
|
|
5
|
+
A command-line tool that analyzes your AWS, GCP, and Azure bills to identify cost-saving opportunities — idle resources, oversized instances, unattached volumes, and more.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
Cloud bills are growing faster than revenue. Engineering teams overprovision, forget to clean up dev resources, and lack visibility into what's actually costing money. Existing cost management tools are expensive, slow, or buried in complex dashboards.
|
|
12
|
+
|
|
13
|
+
## The Solution
|
|
14
|
+
|
|
15
|
+
`cloud-cost-cli` connects to your cloud accounts, analyzes resource usage and billing data, and outputs a ranked list of actionable savings opportunities — all in your terminal, in under 60 seconds.
|
|
16
|
+
|
|
17
|
+
**What it finds:**
|
|
18
|
+
- Idle EC2/Compute instances (low CPU, stopped but still billed)
|
|
19
|
+
- Unattached EBS volumes and snapshots
|
|
20
|
+
- Oversized RDS/database instances
|
|
21
|
+
- Old load balancers with no traffic
|
|
22
|
+
- Unused Elastic IPs
|
|
23
|
+
- Underutilized reserved instances or savings plan mismatches
|
|
24
|
+
- Redundant S3 storage (lifecycle policies missing)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features (MVP - Week 1–2)
|
|
29
|
+
|
|
30
|
+
- ✅ AWS support (EC2, EBS, RDS, S3, ELB, Elastic IP)
|
|
31
|
+
- ✅ Connect via AWS credentials (IAM read-only recommended)
|
|
32
|
+
- ✅ Analyze last 30 days of usage
|
|
33
|
+
- ✅ Output top 5 savings opportunities with estimated monthly savings
|
|
34
|
+
- ✅ Export report as JSON, Markdown, or terminal table
|
|
35
|
+
- ✅ Zero third-party API dependencies (uses AWS SDK directly)
|
|
36
|
+
|
|
37
|
+
**Coming soon:**
|
|
38
|
+
- GCP and Azure support
|
|
39
|
+
- Slack/email alerts for new cost spikes
|
|
40
|
+
- Scheduled reports (cron-friendly)
|
|
41
|
+
- Team dashboard (web UI, SaaS)
|
|
42
|
+
- Custom rules and thresholds
|
|
43
|
+
- Terraform/IaC integration (flag risky configs before apply)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
**Requirements:**
|
|
50
|
+
- Node.js >= 18 (or use the standalone binary)
|
|
51
|
+
- AWS credentials configured (see [AWS CLI setup](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html))
|
|
52
|
+
|
|
53
|
+
**Install via npm:**
|
|
54
|
+
```bash
|
|
55
|
+
npm install -g cloud-cost-cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Or download standalone binary:**
|
|
59
|
+
```bash
|
|
60
|
+
# macOS/Linux
|
|
61
|
+
curl -L https://github.com/vuhp/cloud-cost-cli/releases/latest/download/cloud-cost-cli-$(uname -s)-$(uname -m) -o /usr/local/bin/cloud-cost-cli
|
|
62
|
+
chmod +x /usr/local/bin/cloud-cost-cli
|
|
63
|
+
|
|
64
|
+
# Windows (PowerShell)
|
|
65
|
+
Invoke-WebRequest -Uri "https://github.com/vuhp/cloud-cost-cli/releases/latest/download/cloud-cost-cli-windows-x64.exe" -OutFile "$env:USERPROFILE\bin\cloud-cost-cli.exe"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
**Basic scan (AWS):**
|
|
73
|
+
```bash
|
|
74
|
+
cloud-cost-cli scan --provider aws --profile default
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Specify region and output format:**
|
|
78
|
+
```bash
|
|
79
|
+
cloud-cost-cli scan --provider aws --region us-east-1 --output json > report.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Top N opportunities:**
|
|
83
|
+
```bash
|
|
84
|
+
cloud-cost-cli scan --provider aws --top 10
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Example output:**
|
|
88
|
+
```
|
|
89
|
+
Cloud Cost Optimization Report
|
|
90
|
+
Provider: AWS | Region: us-east-1 | Account: 123456789012
|
|
91
|
+
Analyzed: 2026-01-01 to 2026-01-31
|
|
92
|
+
|
|
93
|
+
Top 5 Savings Opportunities (est. $1,245/month):
|
|
94
|
+
|
|
95
|
+
1. Idle EC2 instance: i-0abc123def456
|
|
96
|
+
Type: m5.large | Running 720h | Avg CPU: 2%
|
|
97
|
+
→ Recommendation: Stop or downsize to t3.small
|
|
98
|
+
→ Est. savings: $65/month
|
|
99
|
+
|
|
100
|
+
2. Unattached EBS volume: vol-0xyz789abc
|
|
101
|
+
Size: 500 GB (gp3) | Age: 45 days
|
|
102
|
+
→ Recommendation: Delete or snapshot + delete
|
|
103
|
+
→ Est. savings: $40/month
|
|
104
|
+
|
|
105
|
+
3. Oversized RDS instance: mydb-production
|
|
106
|
+
Type: db.r5.xlarge | Avg connections: 5 | Avg CPU: 15%
|
|
107
|
+
→ Recommendation: Downsize to db.t3.large
|
|
108
|
+
→ Est. savings: $180/month
|
|
109
|
+
|
|
110
|
+
4. Unused Elastic Load Balancer: my-old-alb
|
|
111
|
+
Active targets: 0 | Requests/day: 0
|
|
112
|
+
→ Recommendation: Delete
|
|
113
|
+
→ Est. savings: $22/month
|
|
114
|
+
|
|
115
|
+
5. S3 bucket with no lifecycle policy: logs-bucket-2023
|
|
116
|
+
Size: 12 TB | Age: 18 months
|
|
117
|
+
→ Recommendation: Enable Intelligent-Tiering or Glacier transition
|
|
118
|
+
→ Est. savings: $938/month
|
|
119
|
+
|
|
120
|
+
Total potential savings: $1,245/month ($14,940/year)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Create a config file at `~/.cloud-cost-cli/config.json` (optional):
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"aws": {
|
|
132
|
+
"profile": "default",
|
|
133
|
+
"regions": ["us-east-1", "us-west-2"],
|
|
134
|
+
"excludeResources": ["i-0abc123", "vol-xyz"],
|
|
135
|
+
"thresholds": {
|
|
136
|
+
"idleCpuPercent": 5,
|
|
137
|
+
"minAgeDays": 7
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"output": {
|
|
141
|
+
"format": "table",
|
|
142
|
+
"includeRecommendations": true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Roadmap
|
|
150
|
+
|
|
151
|
+
**Phase 1 (Week 1–2): MVP**
|
|
152
|
+
- AWS support (EC2, EBS, RDS, S3, ELB, EIP)
|
|
153
|
+
- CLI with basic scan and reporting
|
|
154
|
+
- GitHub Sponsors setup
|
|
155
|
+
- Launch on Hacker News, Product Hunt, Reddit
|
|
156
|
+
|
|
157
|
+
**Phase 2 (Month 2):**
|
|
158
|
+
- GCP support
|
|
159
|
+
- Azure support
|
|
160
|
+
- Scheduled reports (cron mode)
|
|
161
|
+
- Slack webhook integration
|
|
162
|
+
- GitHub Sponsors perks (early access, priority support)
|
|
163
|
+
|
|
164
|
+
**Phase 3 (Month 3+):**
|
|
165
|
+
- SaaS version: connect account, weekly email reports, team dashboard
|
|
166
|
+
- Paid tiers: $29/mo (alerts + history), $99/mo (multi-account + API)
|
|
167
|
+
- Enterprise: on-prem, SSO, audit logs, custom rules
|
|
168
|
+
- Terraform provider (flag cost risks before apply)
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Contributions welcome! Please open an issue before submitting large PRs.
|
|
175
|
+
|
|
176
|
+
**Development setup:**
|
|
177
|
+
```bash
|
|
178
|
+
git clone https://github.com/vuhp/cloud-cost-cli.git
|
|
179
|
+
cd cloud-cost-cli
|
|
180
|
+
npm install
|
|
181
|
+
npm run dev
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Run tests:**
|
|
185
|
+
```bash
|
|
186
|
+
npm test
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Sponsorship
|
|
192
|
+
|
|
193
|
+
If this tool saves you money, consider sponsoring development via [GitHub Sponsors](https://github.com/sponsors/vuhp).
|
|
194
|
+
|
|
195
|
+
**Sponsor tiers:**
|
|
196
|
+
- ☕ $5/mo: Thank you!
|
|
197
|
+
- 🚀 $25/mo: Early access to new features + priority support
|
|
198
|
+
- 💼 $100/mo: Logo on README + monthly 1:1 consultation (30 min)
|
|
199
|
+
- 🏢 $500/mo: Custom rule development + dedicated Slack channel
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT License - see [LICENSE](LICENSE)
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Credits
|
|
210
|
+
|
|
211
|
+
**Powered by:**
|
|
212
|
+
- AWS SDK for JavaScript
|
|
213
|
+
- Commander.js
|
|
214
|
+
- Chalk (terminal colors)
|
|
215
|
+
- Table (terminal tables)
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## FAQ
|
|
220
|
+
|
|
221
|
+
**Q: Does this tool make changes to my infrastructure?**
|
|
222
|
+
A: No. It only reads billing and usage data. It never modifies or deletes resources.
|
|
223
|
+
|
|
224
|
+
**Q: What IAM permissions are required?**
|
|
225
|
+
A: Read-only permissions for Cost Explorer, EC2, EBS, RDS, S3, ELB. See [IAM policy template](docs/iam-policy.json).
|
|
226
|
+
|
|
227
|
+
**Q: How accurate are the savings estimates?**
|
|
228
|
+
A: Estimates are based on current pricing and usage patterns. Actual savings may vary.
|
|
229
|
+
|
|
230
|
+
**Q: Can I run this in CI/CD?**
|
|
231
|
+
A: Yes. Use `--output json` and fail the build if savings exceed a threshold.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
**Star this repo if it saves you money!** ⭐
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const scan_js_1 = require("../src/commands/scan.js");
|
|
6
|
+
const program = new commander_1.Command();
|
|
7
|
+
program
|
|
8
|
+
.name('cloud-cost-cli')
|
|
9
|
+
.description('Optimize your cloud spend in seconds')
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
program
|
|
12
|
+
.command('scan')
|
|
13
|
+
.description('Scan cloud account for cost savings')
|
|
14
|
+
.option('--provider <aws|gcp|azure>', 'Cloud provider', 'aws')
|
|
15
|
+
.option('--region <region>', 'Cloud region (e.g., us-east-1)')
|
|
16
|
+
.option('--profile <profile>', 'AWS profile name', 'default')
|
|
17
|
+
.option('--top <N>', 'Show top N opportunities', '5')
|
|
18
|
+
.option('--output <table|json|markdown>', 'Output format', 'table')
|
|
19
|
+
.option('--days <N>', 'Analysis period in days', '30')
|
|
20
|
+
.option('--min-savings <amount>', 'Filter by minimum savings ($/month)')
|
|
21
|
+
.option('--verbose', 'Verbose logging')
|
|
22
|
+
.action(scan_js_1.scanCommand);
|
|
23
|
+
program.parse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const EC2_PRICING: Record<string, number>;
|
|
2
|
+
export declare const EBS_PRICING: Record<string, number>;
|
|
3
|
+
export declare const RDS_PRICING: Record<string, number>;
|
|
4
|
+
export declare const S3_PRICING: {
|
|
5
|
+
standard: number;
|
|
6
|
+
intelligentTiering: number;
|
|
7
|
+
glacier: number;
|
|
8
|
+
glacierDeepArchive: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const ELB_PRICING: {
|
|
11
|
+
alb: number;
|
|
12
|
+
nlb: number;
|
|
13
|
+
clb: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const EIP_PRICING_HOURLY = 0.005;
|
|
16
|
+
export declare function getEC2MonthlyCost(instanceType: string): number;
|
|
17
|
+
export declare function getEBSMonthlyCost(sizeGB: number, volumeType: string): number;
|
|
18
|
+
export declare function getRDSMonthlyCost(instanceClass: string): number;
|
|
19
|
+
export declare function getS3MonthlyCost(sizeGB: number, storageClass?: string): number;
|
|
20
|
+
export declare function getELBMonthlyCost(type?: 'alb' | 'nlb' | 'clb'): number;
|
|
21
|
+
export declare function getEIPMonthlyCost(): number;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// EC2 instance pricing (us-east-1, Linux, on-demand, monthly estimate)
|
|
3
|
+
// Source: AWS pricing as of 2026-01, monthly = hourly × 730
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.EIP_PRICING_HOURLY = exports.ELB_PRICING = exports.S3_PRICING = exports.RDS_PRICING = exports.EBS_PRICING = exports.EC2_PRICING = void 0;
|
|
6
|
+
exports.getEC2MonthlyCost = getEC2MonthlyCost;
|
|
7
|
+
exports.getEBSMonthlyCost = getEBSMonthlyCost;
|
|
8
|
+
exports.getRDSMonthlyCost = getRDSMonthlyCost;
|
|
9
|
+
exports.getS3MonthlyCost = getS3MonthlyCost;
|
|
10
|
+
exports.getELBMonthlyCost = getELBMonthlyCost;
|
|
11
|
+
exports.getEIPMonthlyCost = getEIPMonthlyCost;
|
|
12
|
+
exports.EC2_PRICING = {
|
|
13
|
+
't2.micro': 8.47,
|
|
14
|
+
't2.small': 16.79,
|
|
15
|
+
't2.medium': 33.87,
|
|
16
|
+
't3.micro': 7.59,
|
|
17
|
+
't3.small': 15.18,
|
|
18
|
+
't3.medium': 30.37,
|
|
19
|
+
't3.large': 60.74,
|
|
20
|
+
't3.xlarge': 121.47,
|
|
21
|
+
'm5.large': 70.08,
|
|
22
|
+
'm5.xlarge': 140.16,
|
|
23
|
+
'm5.2xlarge': 280.32,
|
|
24
|
+
'm6i.large': 69.35,
|
|
25
|
+
'm6i.xlarge': 138.70,
|
|
26
|
+
'c5.large': 62.05,
|
|
27
|
+
'c5.xlarge': 124.10,
|
|
28
|
+
'r5.large': 91.25,
|
|
29
|
+
'r5.xlarge': 182.50,
|
|
30
|
+
};
|
|
31
|
+
// EBS pricing (us-east-1, per GB/month)
|
|
32
|
+
exports.EBS_PRICING = {
|
|
33
|
+
gp3: 0.08,
|
|
34
|
+
gp2: 0.10,
|
|
35
|
+
io1: 0.125,
|
|
36
|
+
io2: 0.125,
|
|
37
|
+
st1: 0.045,
|
|
38
|
+
sc1: 0.015,
|
|
39
|
+
};
|
|
40
|
+
// RDS pricing (us-east-1, per month)
|
|
41
|
+
exports.RDS_PRICING = {
|
|
42
|
+
'db.t3.micro': 11.01,
|
|
43
|
+
'db.t3.small': 22.63,
|
|
44
|
+
'db.t3.medium': 45.26,
|
|
45
|
+
'db.t3.large': 90.51,
|
|
46
|
+
'db.m5.large': 127.75,
|
|
47
|
+
'db.m5.xlarge': 255.50,
|
|
48
|
+
'db.r5.large': 175.20,
|
|
49
|
+
'db.r5.xlarge': 350.40,
|
|
50
|
+
'db.r5.2xlarge': 700.80,
|
|
51
|
+
};
|
|
52
|
+
// S3 pricing (per GB/month)
|
|
53
|
+
exports.S3_PRICING = {
|
|
54
|
+
standard: 0.023,
|
|
55
|
+
intelligentTiering: 0.023, // same base, but auto-transitions
|
|
56
|
+
glacier: 0.004,
|
|
57
|
+
glacierDeepArchive: 0.00099,
|
|
58
|
+
};
|
|
59
|
+
// ELB pricing (per month, excluding LCU charges)
|
|
60
|
+
exports.ELB_PRICING = {
|
|
61
|
+
alb: 16.43, // Application Load Balancer
|
|
62
|
+
nlb: 16.43, // Network Load Balancer
|
|
63
|
+
clb: 18.25, // Classic Load Balancer
|
|
64
|
+
};
|
|
65
|
+
// Elastic IP pricing (per hour when not associated)
|
|
66
|
+
exports.EIP_PRICING_HOURLY = 0.005; // ~$3.65/month
|
|
67
|
+
function getEC2MonthlyCost(instanceType) {
|
|
68
|
+
return exports.EC2_PRICING[instanceType] || 0;
|
|
69
|
+
}
|
|
70
|
+
function getEBSMonthlyCost(sizeGB, volumeType) {
|
|
71
|
+
const pricePerGB = exports.EBS_PRICING[volumeType] || 0.08;
|
|
72
|
+
return sizeGB * pricePerGB;
|
|
73
|
+
}
|
|
74
|
+
function getRDSMonthlyCost(instanceClass) {
|
|
75
|
+
return exports.RDS_PRICING[instanceClass] || 0;
|
|
76
|
+
}
|
|
77
|
+
function getS3MonthlyCost(sizeGB, storageClass = 'standard') {
|
|
78
|
+
const pricePerGB = exports.S3_PRICING[storageClass] || exports.S3_PRICING.standard;
|
|
79
|
+
return sizeGB * pricePerGB;
|
|
80
|
+
}
|
|
81
|
+
function getELBMonthlyCost(type = 'alb') {
|
|
82
|
+
return exports.ELB_PRICING[type];
|
|
83
|
+
}
|
|
84
|
+
function getEIPMonthlyCost() {
|
|
85
|
+
return exports.EIP_PRICING_HOURLY * 730; // ~$3.65
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface ScanCommandOptions {
|
|
2
|
+
provider: string;
|
|
3
|
+
region?: string;
|
|
4
|
+
profile?: string;
|
|
5
|
+
top?: string;
|
|
6
|
+
output?: string;
|
|
7
|
+
days?: string;
|
|
8
|
+
minSavings?: string;
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function scanCommand(options: ScanCommandOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanCommand = scanCommand;
|
|
4
|
+
const client_1 = require("../providers/aws/client");
|
|
5
|
+
const ec2_1 = require("../providers/aws/ec2");
|
|
6
|
+
const ebs_1 = require("../providers/aws/ebs");
|
|
7
|
+
const rds_1 = require("../providers/aws/rds");
|
|
8
|
+
const s3_1 = require("../providers/aws/s3");
|
|
9
|
+
const elb_1 = require("../providers/aws/elb");
|
|
10
|
+
const eip_1 = require("../providers/aws/eip");
|
|
11
|
+
const table_1 = require("../reporters/table");
|
|
12
|
+
const json_1 = require("../reporters/json");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
async function scanCommand(options) {
|
|
15
|
+
try {
|
|
16
|
+
if (options.provider !== 'aws') {
|
|
17
|
+
(0, logger_1.error)(`Provider "${options.provider}" not yet supported. Use --provider aws`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
(0, logger_1.info)(`Scanning AWS account (profile: ${options.profile || 'default'}, region: ${options.region || 'us-east-1'})...`);
|
|
21
|
+
const client = new client_1.AWSClient({
|
|
22
|
+
region: options.region,
|
|
23
|
+
profile: options.profile,
|
|
24
|
+
});
|
|
25
|
+
// Run analyzers in parallel
|
|
26
|
+
(0, logger_1.info)('Analyzing EC2 instances...');
|
|
27
|
+
const ec2Promise = (0, ec2_1.analyzeEC2Instances)(client);
|
|
28
|
+
(0, logger_1.info)('Analyzing EBS volumes...');
|
|
29
|
+
const ebsPromise = (0, ebs_1.analyzeEBSVolumes)(client);
|
|
30
|
+
(0, logger_1.info)('Analyzing RDS instances...');
|
|
31
|
+
const rdsPromise = (0, rds_1.analyzeRDSInstances)(client);
|
|
32
|
+
(0, logger_1.info)('Analyzing S3 buckets...');
|
|
33
|
+
const s3Promise = (0, s3_1.analyzeS3Buckets)(client);
|
|
34
|
+
(0, logger_1.info)('Analyzing Load Balancers...');
|
|
35
|
+
const elbPromise = (0, elb_1.analyzeELBs)(client);
|
|
36
|
+
(0, logger_1.info)('Analyzing Elastic IPs...');
|
|
37
|
+
const eipPromise = (0, eip_1.analyzeElasticIPs)(client);
|
|
38
|
+
// Wait for all analyzers to complete
|
|
39
|
+
const [ec2Opportunities, ebsOpportunities, rdsOpportunities, s3Opportunities, elbOpportunities, eipOpportunities,] = await Promise.all([
|
|
40
|
+
ec2Promise,
|
|
41
|
+
ebsPromise,
|
|
42
|
+
rdsPromise,
|
|
43
|
+
s3Promise,
|
|
44
|
+
elbPromise,
|
|
45
|
+
eipPromise,
|
|
46
|
+
]);
|
|
47
|
+
(0, logger_1.success)(`Found ${ec2Opportunities.length} EC2 opportunities`);
|
|
48
|
+
(0, logger_1.success)(`Found ${ebsOpportunities.length} EBS opportunities`);
|
|
49
|
+
(0, logger_1.success)(`Found ${rdsOpportunities.length} RDS opportunities`);
|
|
50
|
+
(0, logger_1.success)(`Found ${s3Opportunities.length} S3 opportunities`);
|
|
51
|
+
(0, logger_1.success)(`Found ${elbOpportunities.length} ELB opportunities`);
|
|
52
|
+
(0, logger_1.success)(`Found ${eipOpportunities.length} EIP opportunities`);
|
|
53
|
+
// Combine opportunities
|
|
54
|
+
const allOpportunities = [
|
|
55
|
+
...ec2Opportunities,
|
|
56
|
+
...ebsOpportunities,
|
|
57
|
+
...rdsOpportunities,
|
|
58
|
+
...s3Opportunities,
|
|
59
|
+
...elbOpportunities,
|
|
60
|
+
...eipOpportunities,
|
|
61
|
+
];
|
|
62
|
+
// Filter by minimum savings if specified
|
|
63
|
+
const minSavings = options.minSavings ? parseFloat(options.minSavings) : 0;
|
|
64
|
+
const filteredOpportunities = allOpportunities.filter((opp) => opp.estimatedSavings >= minSavings);
|
|
65
|
+
// Calculate totals
|
|
66
|
+
const totalPotentialSavings = filteredOpportunities.reduce((sum, opp) => sum + opp.estimatedSavings, 0);
|
|
67
|
+
const summary = {
|
|
68
|
+
totalResources: filteredOpportunities.length,
|
|
69
|
+
idleResources: filteredOpportunities.filter((o) => o.category === 'idle').length,
|
|
70
|
+
oversizedResources: filteredOpportunities.filter((o) => o.category === 'oversized').length,
|
|
71
|
+
unusedResources: filteredOpportunities.filter((o) => o.category === 'unused').length,
|
|
72
|
+
};
|
|
73
|
+
const report = {
|
|
74
|
+
provider: 'aws',
|
|
75
|
+
accountId: 'N/A', // Will fetch from STS in future
|
|
76
|
+
region: client.region,
|
|
77
|
+
scanPeriod: {
|
|
78
|
+
start: new Date(Date.now() - (parseInt(options.days || '30') * 24 * 60 * 60 * 1000)),
|
|
79
|
+
end: new Date(),
|
|
80
|
+
},
|
|
81
|
+
opportunities: filteredOpportunities,
|
|
82
|
+
totalPotentialSavings,
|
|
83
|
+
summary,
|
|
84
|
+
};
|
|
85
|
+
// Render output
|
|
86
|
+
const topN = parseInt(options.top || '5');
|
|
87
|
+
if (options.output === 'json') {
|
|
88
|
+
(0, json_1.renderJSON)(report);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
(0, table_1.renderTable)(report, topN);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (err.name === 'CredentialsProviderError' || err.message?.includes('credentials')) {
|
|
96
|
+
(0, logger_1.error)('AWS credentials not found or invalid.');
|
|
97
|
+
console.log('\nTo configure AWS credentials:');
|
|
98
|
+
console.log('1. Run: aws configure');
|
|
99
|
+
console.log('2. Or set environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY');
|
|
100
|
+
console.log('3. Or use --profile flag with a configured profile');
|
|
101
|
+
console.log('\nSee: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
(0, logger_1.error)(`Scan failed: ${err.message}`);
|
|
105
|
+
if (options.verbose) {
|
|
106
|
+
console.error(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EC2Client } from '@aws-sdk/client-ec2';
|
|
2
|
+
import { CloudWatchClient } from '@aws-sdk/client-cloudwatch';
|
|
3
|
+
import { CostExplorerClient } from '@aws-sdk/client-cost-explorer';
|
|
4
|
+
import { RDSClient } from '@aws-sdk/client-rds';
|
|
5
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
6
|
+
import { ElasticLoadBalancingV2Client } from '@aws-sdk/client-elastic-load-balancing-v2';
|
|
7
|
+
export interface AWSClientOptions {
|
|
8
|
+
region?: string;
|
|
9
|
+
profile?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class AWSClient {
|
|
12
|
+
private ec2;
|
|
13
|
+
private cloudwatch;
|
|
14
|
+
private costExplorer;
|
|
15
|
+
private rds;
|
|
16
|
+
private s3;
|
|
17
|
+
private elb;
|
|
18
|
+
region: string;
|
|
19
|
+
profile: string;
|
|
20
|
+
constructor(options: AWSClientOptions);
|
|
21
|
+
getEC2Client(): EC2Client;
|
|
22
|
+
getCloudWatchClient(): CloudWatchClient;
|
|
23
|
+
getCostExplorerClient(): CostExplorerClient;
|
|
24
|
+
getRDSClient(): RDSClient;
|
|
25
|
+
getS3Client(): S3Client;
|
|
26
|
+
getELBClient(): ElasticLoadBalancingV2Client;
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AWSClient = void 0;
|
|
4
|
+
const client_ec2_1 = require("@aws-sdk/client-ec2");
|
|
5
|
+
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
6
|
+
const client_cost_explorer_1 = require("@aws-sdk/client-cost-explorer");
|
|
7
|
+
const client_rds_1 = require("@aws-sdk/client-rds");
|
|
8
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
9
|
+
const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
10
|
+
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
11
|
+
class AWSClient {
|
|
12
|
+
ec2;
|
|
13
|
+
cloudwatch;
|
|
14
|
+
costExplorer;
|
|
15
|
+
rds;
|
|
16
|
+
s3;
|
|
17
|
+
elb;
|
|
18
|
+
region;
|
|
19
|
+
profile;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.region = options.region || 'us-east-1';
|
|
22
|
+
this.profile = options.profile || 'default';
|
|
23
|
+
// Use environment credentials if available, otherwise fall back to profile
|
|
24
|
+
const credentials = process.env.AWS_ACCESS_KEY_ID
|
|
25
|
+
? (0, credential_providers_1.fromEnv)()
|
|
26
|
+
: (0, credential_providers_1.fromIni)({ profile: this.profile });
|
|
27
|
+
this.ec2 = new client_ec2_1.EC2Client({ region: this.region, credentials });
|
|
28
|
+
this.cloudwatch = new client_cloudwatch_1.CloudWatchClient({ region: this.region, credentials });
|
|
29
|
+
this.costExplorer = new client_cost_explorer_1.CostExplorerClient({ region: 'us-east-1', credentials });
|
|
30
|
+
this.rds = new client_rds_1.RDSClient({ region: this.region, credentials });
|
|
31
|
+
this.s3 = new client_s3_1.S3Client({ region: this.region, credentials });
|
|
32
|
+
this.elb = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client({ region: this.region, credentials });
|
|
33
|
+
}
|
|
34
|
+
getEC2Client() {
|
|
35
|
+
return this.ec2;
|
|
36
|
+
}
|
|
37
|
+
getCloudWatchClient() {
|
|
38
|
+
return this.cloudwatch;
|
|
39
|
+
}
|
|
40
|
+
getCostExplorerClient() {
|
|
41
|
+
return this.costExplorer;
|
|
42
|
+
}
|
|
43
|
+
getRDSClient() {
|
|
44
|
+
return this.rds;
|
|
45
|
+
}
|
|
46
|
+
getS3Client() {
|
|
47
|
+
return this.s3;
|
|
48
|
+
}
|
|
49
|
+
getELBClient() {
|
|
50
|
+
return this.elb;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.AWSClient = AWSClient;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeEBSVolumes = analyzeEBSVolumes;
|
|
4
|
+
const client_ec2_1 = require("@aws-sdk/client-ec2");
|
|
5
|
+
const cost_estimator_1 = require("../../analyzers/cost-estimator");
|
|
6
|
+
const formatter_1 = require("../../utils/formatter");
|
|
7
|
+
async function analyzeEBSVolumes(client) {
|
|
8
|
+
const ec2Client = client.getEC2Client();
|
|
9
|
+
const result = await ec2Client.send(new client_ec2_1.DescribeVolumesCommand({
|
|
10
|
+
Filters: [{ Name: 'status', Values: ['available'] }],
|
|
11
|
+
}));
|
|
12
|
+
const volumes = result.Volumes || [];
|
|
13
|
+
const opportunities = [];
|
|
14
|
+
for (const volume of volumes) {
|
|
15
|
+
if (!volume.VolumeId || !volume.Size || !volume.CreateTime)
|
|
16
|
+
continue;
|
|
17
|
+
const age = (0, formatter_1.daysSince)(volume.CreateTime);
|
|
18
|
+
if (age > 7) {
|
|
19
|
+
const monthlyCost = (0, cost_estimator_1.getEBSMonthlyCost)(volume.Size, volume.VolumeType || 'gp3');
|
|
20
|
+
opportunities.push({
|
|
21
|
+
id: `ebs-unattached-${volume.VolumeId}`,
|
|
22
|
+
provider: 'aws',
|
|
23
|
+
resourceType: 'ebs',
|
|
24
|
+
resourceId: volume.VolumeId,
|
|
25
|
+
resourceName: volume.Tags?.find((t) => t.Key === 'Name')?.Value,
|
|
26
|
+
category: 'unused',
|
|
27
|
+
currentCost: monthlyCost,
|
|
28
|
+
estimatedSavings: monthlyCost,
|
|
29
|
+
confidence: 'high',
|
|
30
|
+
recommendation: `Snapshot and delete, or delete if redundant (age: ${age} days)`,
|
|
31
|
+
metadata: {
|
|
32
|
+
size: volume.Size,
|
|
33
|
+
volumeType: volume.VolumeType,
|
|
34
|
+
age,
|
|
35
|
+
},
|
|
36
|
+
detectedAt: new Date(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return opportunities;
|
|
41
|
+
}
|