cloud-cost-cli 0.1.0 → 0.2.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/README.md +119 -122
- package/dist/bin/cloud-cost-cli.js +5 -2
- package/dist/src/analyzers/cost-estimator.d.ts +14 -0
- package/dist/src/analyzers/cost-estimator.js +74 -16
- package/dist/src/analyzers/pricing-service.d.ts +38 -0
- package/dist/src/analyzers/pricing-service.js +263 -0
- package/dist/src/commands/scan.d.ts +3 -0
- package/dist/src/commands/scan.js +173 -86
- package/dist/src/providers/aws/client.d.ts +1 -0
- package/dist/src/providers/aws/client.js +60 -1
- package/dist/src/providers/azure/client.d.ts +20 -0
- package/dist/src/providers/azure/client.js +41 -0
- package/dist/src/providers/azure/disks.d.ts +4 -0
- package/dist/src/providers/azure/disks.js +87 -0
- package/dist/src/providers/azure/index.d.ts +6 -0
- package/dist/src/providers/azure/index.js +15 -0
- package/dist/src/providers/azure/public-ips.d.ts +3 -0
- package/dist/src/providers/azure/public-ips.js +47 -0
- package/dist/src/providers/azure/sql.d.ts +4 -0
- package/dist/src/providers/azure/sql.js +134 -0
- package/dist/src/providers/azure/storage.d.ts +8 -0
- package/dist/src/providers/azure/storage.js +100 -0
- package/dist/src/providers/azure/vms.d.ts +4 -0
- package/dist/src/providers/azure/vms.js +164 -0
- package/dist/src/reporters/table.js +3 -1
- package/dist/src/utils/formatter.d.ts +2 -0
- package/dist/src/utils/formatter.js +29 -1
- package/docs/RELEASE.md +14 -22
- package/package.json +12 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PricingService = void 0;
|
|
4
|
+
exports.getS3MonthlyCost = getS3MonthlyCost;
|
|
5
|
+
exports.getELBMonthlyCost = getELBMonthlyCost;
|
|
6
|
+
exports.getEIPMonthlyCost = getEIPMonthlyCost;
|
|
7
|
+
const client_pricing_1 = require("@aws-sdk/client-pricing");
|
|
8
|
+
// Cache for pricing data to avoid repeated API calls
|
|
9
|
+
const pricingCache = new Map();
|
|
10
|
+
class PricingService {
|
|
11
|
+
pricingClient;
|
|
12
|
+
region;
|
|
13
|
+
constructor(region = 'us-east-1') {
|
|
14
|
+
this.region = region;
|
|
15
|
+
// Pricing API is only available in us-east-1 and ap-south-1
|
|
16
|
+
this.pricingClient = new client_pricing_1.PricingClient({ region: 'us-east-1' });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get EC2 instance on-demand pricing for the configured region
|
|
20
|
+
*/
|
|
21
|
+
async getEC2Price(instanceType) {
|
|
22
|
+
const cacheKey = `ec2-${this.region}-${instanceType}`;
|
|
23
|
+
if (pricingCache.has(cacheKey)) {
|
|
24
|
+
return pricingCache.get(cacheKey);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
|
|
28
|
+
ServiceCode: 'AmazonEC2',
|
|
29
|
+
Filters: [
|
|
30
|
+
{
|
|
31
|
+
Type: 'TERM_MATCH',
|
|
32
|
+
Field: 'instanceType',
|
|
33
|
+
Value: instanceType,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
Type: 'TERM_MATCH',
|
|
37
|
+
Field: 'location',
|
|
38
|
+
Value: this.getLocationName(this.region),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
Type: 'TERM_MATCH',
|
|
42
|
+
Field: 'tenancy',
|
|
43
|
+
Value: 'Shared',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
Type: 'TERM_MATCH',
|
|
47
|
+
Field: 'operatingSystem',
|
|
48
|
+
Value: 'Linux',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
Type: 'TERM_MATCH',
|
|
52
|
+
Field: 'preInstalledSw',
|
|
53
|
+
Value: 'NA',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
Type: 'TERM_MATCH',
|
|
57
|
+
Field: 'capacitystatus',
|
|
58
|
+
Value: 'Used',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
MaxResults: 1,
|
|
62
|
+
}));
|
|
63
|
+
const hourlyPrice = this.extractPriceFromResponse(response.PriceList);
|
|
64
|
+
const monthlyPrice = hourlyPrice * 730; // 730 hours per month
|
|
65
|
+
pricingCache.set(cacheKey, monthlyPrice);
|
|
66
|
+
return monthlyPrice;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// Fallback to estimate if API fails
|
|
70
|
+
return this.getFallbackEC2Price(instanceType);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get EBS volume pricing per GB
|
|
75
|
+
*/
|
|
76
|
+
async getEBSPrice(volumeType) {
|
|
77
|
+
const cacheKey = `ebs-${this.region}-${volumeType}`;
|
|
78
|
+
if (pricingCache.has(cacheKey)) {
|
|
79
|
+
return pricingCache.get(cacheKey);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
|
|
83
|
+
ServiceCode: 'AmazonEC2',
|
|
84
|
+
Filters: [
|
|
85
|
+
{
|
|
86
|
+
Type: 'TERM_MATCH',
|
|
87
|
+
Field: 'productFamily',
|
|
88
|
+
Value: 'Storage',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
Type: 'TERM_MATCH',
|
|
92
|
+
Field: 'volumeApiName',
|
|
93
|
+
Value: volumeType,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
Type: 'TERM_MATCH',
|
|
97
|
+
Field: 'location',
|
|
98
|
+
Value: this.getLocationName(this.region),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
MaxResults: 1,
|
|
102
|
+
}));
|
|
103
|
+
const pricePerGB = this.extractPriceFromResponse(response.PriceList);
|
|
104
|
+
pricingCache.set(cacheKey, pricePerGB);
|
|
105
|
+
return pricePerGB;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return this.getFallbackEBSPrice(volumeType);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get RDS instance pricing
|
|
113
|
+
*/
|
|
114
|
+
async getRDSPrice(instanceClass, engine = 'mysql') {
|
|
115
|
+
const cacheKey = `rds-${this.region}-${instanceClass}-${engine}`;
|
|
116
|
+
if (pricingCache.has(cacheKey)) {
|
|
117
|
+
return pricingCache.get(cacheKey);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
|
|
121
|
+
ServiceCode: 'AmazonRDS',
|
|
122
|
+
Filters: [
|
|
123
|
+
{
|
|
124
|
+
Type: 'TERM_MATCH',
|
|
125
|
+
Field: 'instanceType',
|
|
126
|
+
Value: instanceClass,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
Type: 'TERM_MATCH',
|
|
130
|
+
Field: 'location',
|
|
131
|
+
Value: this.getLocationName(this.region),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
Type: 'TERM_MATCH',
|
|
135
|
+
Field: 'databaseEngine',
|
|
136
|
+
Value: this.normalizeEngine(engine),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
Type: 'TERM_MATCH',
|
|
140
|
+
Field: 'deploymentOption',
|
|
141
|
+
Value: 'Single-AZ',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
MaxResults: 1,
|
|
145
|
+
}));
|
|
146
|
+
const hourlyPrice = this.extractPriceFromResponse(response.PriceList);
|
|
147
|
+
const monthlyPrice = hourlyPrice * 730;
|
|
148
|
+
pricingCache.set(cacheKey, monthlyPrice);
|
|
149
|
+
return monthlyPrice;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return this.getFallbackRDSPrice(instanceClass);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract price from AWS Pricing API response
|
|
157
|
+
*/
|
|
158
|
+
extractPriceFromResponse(priceList) {
|
|
159
|
+
if (!priceList || priceList.length === 0) {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const product = JSON.parse(priceList[0]);
|
|
164
|
+
const terms = product.terms?.OnDemand;
|
|
165
|
+
if (!terms)
|
|
166
|
+
return 0;
|
|
167
|
+
const termKey = Object.keys(terms)[0];
|
|
168
|
+
const priceDimensions = terms[termKey]?.priceDimensions;
|
|
169
|
+
if (!priceDimensions)
|
|
170
|
+
return 0;
|
|
171
|
+
const dimensionKey = Object.keys(priceDimensions)[0];
|
|
172
|
+
const pricePerUnit = priceDimensions[dimensionKey]?.pricePerUnit?.USD;
|
|
173
|
+
return parseFloat(pricePerUnit || '0');
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Convert AWS region code to location name used in Pricing API
|
|
181
|
+
*/
|
|
182
|
+
getLocationName(region) {
|
|
183
|
+
const locationMap = {
|
|
184
|
+
'us-east-1': 'US East (N. Virginia)',
|
|
185
|
+
'us-east-2': 'US East (Ohio)',
|
|
186
|
+
'us-west-1': 'US West (N. California)',
|
|
187
|
+
'us-west-2': 'US West (Oregon)',
|
|
188
|
+
'eu-west-1': 'EU (Ireland)',
|
|
189
|
+
'eu-central-1': 'EU (Frankfurt)',
|
|
190
|
+
'ap-southeast-1': 'Asia Pacific (Singapore)',
|
|
191
|
+
'ap-southeast-2': 'Asia Pacific (Sydney)',
|
|
192
|
+
'ap-northeast-1': 'Asia Pacific (Tokyo)',
|
|
193
|
+
// Add more as needed
|
|
194
|
+
};
|
|
195
|
+
return locationMap[region] || 'US East (N. Virginia)';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Normalize RDS engine name for Pricing API
|
|
199
|
+
*/
|
|
200
|
+
normalizeEngine(engine) {
|
|
201
|
+
const engineMap = {
|
|
202
|
+
'mysql': 'MySQL',
|
|
203
|
+
'postgres': 'PostgreSQL',
|
|
204
|
+
'mariadb': 'MariaDB',
|
|
205
|
+
'aurora': 'Aurora MySQL',
|
|
206
|
+
'aurora-mysql': 'Aurora MySQL',
|
|
207
|
+
'aurora-postgresql': 'Aurora PostgreSQL',
|
|
208
|
+
};
|
|
209
|
+
return engineMap[engine.toLowerCase()] || 'MySQL';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Fallback estimates when API calls fail (based on us-east-1)
|
|
213
|
+
*/
|
|
214
|
+
getFallbackEC2Price(instanceType) {
|
|
215
|
+
const estimates = {
|
|
216
|
+
't3.micro': 7.59,
|
|
217
|
+
't3.small': 15.18,
|
|
218
|
+
't3.medium': 30.37,
|
|
219
|
+
't3.large': 60.74,
|
|
220
|
+
'm5.large': 70.08,
|
|
221
|
+
'm5.xlarge': 140.16,
|
|
222
|
+
};
|
|
223
|
+
return estimates[instanceType] || 50; // Generic estimate
|
|
224
|
+
}
|
|
225
|
+
getFallbackEBSPrice(volumeType) {
|
|
226
|
+
const estimates = {
|
|
227
|
+
'gp3': 0.08,
|
|
228
|
+
'gp2': 0.10,
|
|
229
|
+
'io1': 0.125,
|
|
230
|
+
'io2': 0.125,
|
|
231
|
+
};
|
|
232
|
+
return estimates[volumeType] || 0.08;
|
|
233
|
+
}
|
|
234
|
+
getFallbackRDSPrice(instanceClass) {
|
|
235
|
+
const estimates = {
|
|
236
|
+
'db.t3.micro': 11.01,
|
|
237
|
+
'db.t3.small': 22.63,
|
|
238
|
+
'db.t3.medium': 45.26,
|
|
239
|
+
'db.t3.large': 90.51,
|
|
240
|
+
};
|
|
241
|
+
return estimates[instanceClass] || 100;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.PricingService = PricingService;
|
|
245
|
+
// Simple estimates for services where Pricing API is complex (S3, ELB, EIP)
|
|
246
|
+
function getS3MonthlyCost(sizeGB, storageClass = 'standard') {
|
|
247
|
+
const pricing = {
|
|
248
|
+
'STANDARD': 0.023,
|
|
249
|
+
'INTELLIGENT_TIERING': 0.023,
|
|
250
|
+
'GLACIER': 0.004,
|
|
251
|
+
'DEEP_ARCHIVE': 0.00099,
|
|
252
|
+
};
|
|
253
|
+
const pricePerGB = pricing[storageClass] || 0.023;
|
|
254
|
+
return sizeGB * pricePerGB;
|
|
255
|
+
}
|
|
256
|
+
function getELBMonthlyCost(type = 'alb') {
|
|
257
|
+
// ELB pricing is relatively consistent across regions (~$16-18/month base)
|
|
258
|
+
return type === 'clb' ? 18.25 : 16.43;
|
|
259
|
+
}
|
|
260
|
+
function getEIPMonthlyCost() {
|
|
261
|
+
// ~$3.65/month when unattached (consistent across regions)
|
|
262
|
+
return 3.65;
|
|
263
|
+
}
|
|
@@ -2,11 +2,14 @@ interface ScanCommandOptions {
|
|
|
2
2
|
provider: string;
|
|
3
3
|
region?: string;
|
|
4
4
|
profile?: string;
|
|
5
|
+
subscriptionId?: string;
|
|
6
|
+
location?: string;
|
|
5
7
|
top?: string;
|
|
6
8
|
output?: string;
|
|
7
9
|
days?: string;
|
|
8
10
|
minSavings?: string;
|
|
9
11
|
verbose?: boolean;
|
|
12
|
+
accurate?: boolean;
|
|
10
13
|
}
|
|
11
14
|
export declare function scanCommand(options: ScanCommandOptions): Promise<void>;
|
|
12
15
|
export {};
|
|
@@ -8,104 +8,191 @@ const rds_1 = require("../providers/aws/rds");
|
|
|
8
8
|
const s3_1 = require("../providers/aws/s3");
|
|
9
9
|
const elb_1 = require("../providers/aws/elb");
|
|
10
10
|
const eip_1 = require("../providers/aws/eip");
|
|
11
|
+
const client_2 = require("../providers/azure/client");
|
|
12
|
+
const vms_1 = require("../providers/azure/vms");
|
|
13
|
+
const disks_1 = require("../providers/azure/disks");
|
|
14
|
+
const storage_1 = require("../providers/azure/storage");
|
|
15
|
+
const sql_1 = require("../providers/azure/sql");
|
|
16
|
+
const public_ips_1 = require("../providers/azure/public-ips");
|
|
11
17
|
const table_1 = require("../reporters/table");
|
|
12
18
|
const json_1 = require("../reporters/json");
|
|
13
19
|
const logger_1 = require("../utils/logger");
|
|
14
20
|
async function scanCommand(options) {
|
|
15
21
|
try {
|
|
16
|
-
if (options.provider
|
|
17
|
-
|
|
18
|
-
process.exit(1);
|
|
22
|
+
if (options.provider === 'aws') {
|
|
23
|
+
await scanAWS(options);
|
|
19
24
|
}
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
25
|
+
else if (options.provider === 'azure') {
|
|
26
|
+
await scanAzure(options);
|
|
89
27
|
}
|
|
90
28
|
else {
|
|
91
|
-
(0,
|
|
29
|
+
(0, logger_1.error)(`Provider "${options.provider}" not yet supported. Use --provider aws or --provider azure`);
|
|
30
|
+
process.exit(1);
|
|
92
31
|
}
|
|
93
32
|
}
|
|
94
33
|
catch (err) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.
|
|
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
|
-
}
|
|
34
|
+
(0, logger_1.error)(`Scan failed: ${err.message}`);
|
|
35
|
+
if (options.verbose) {
|
|
36
|
+
console.error(err);
|
|
108
37
|
}
|
|
109
38
|
process.exit(1);
|
|
110
39
|
}
|
|
111
40
|
}
|
|
41
|
+
async function scanAWS(options) {
|
|
42
|
+
const client = new client_1.AWSClient({
|
|
43
|
+
region: options.region,
|
|
44
|
+
profile: options.profile,
|
|
45
|
+
});
|
|
46
|
+
(0, logger_1.info)(`Scanning AWS account (profile: ${options.profile || 'default'}, region: ${client.region})...`);
|
|
47
|
+
if (options.accurate) {
|
|
48
|
+
(0, logger_1.info)('Note: --accurate flag is not yet implemented. Using estimated pricing.');
|
|
49
|
+
(0, logger_1.info)('Real-time pricing will be available in a future release.');
|
|
50
|
+
}
|
|
51
|
+
// Run analyzers in parallel
|
|
52
|
+
(0, logger_1.info)('Analyzing EC2 instances...');
|
|
53
|
+
const ec2Promise = (0, ec2_1.analyzeEC2Instances)(client);
|
|
54
|
+
(0, logger_1.info)('Analyzing EBS volumes...');
|
|
55
|
+
const ebsPromise = (0, ebs_1.analyzeEBSVolumes)(client);
|
|
56
|
+
(0, logger_1.info)('Analyzing RDS instances...');
|
|
57
|
+
const rdsPromise = (0, rds_1.analyzeRDSInstances)(client);
|
|
58
|
+
(0, logger_1.info)('Analyzing S3 buckets...');
|
|
59
|
+
const s3Promise = (0, s3_1.analyzeS3Buckets)(client);
|
|
60
|
+
(0, logger_1.info)('Analyzing Load Balancers...');
|
|
61
|
+
const elbPromise = (0, elb_1.analyzeELBs)(client);
|
|
62
|
+
(0, logger_1.info)('Analyzing Elastic IPs...');
|
|
63
|
+
const eipPromise = (0, eip_1.analyzeElasticIPs)(client);
|
|
64
|
+
// Wait for all analyzers to complete
|
|
65
|
+
const [ec2Opportunities, ebsOpportunities, rdsOpportunities, s3Opportunities, elbOpportunities, eipOpportunities,] = await Promise.all([
|
|
66
|
+
ec2Promise,
|
|
67
|
+
ebsPromise,
|
|
68
|
+
rdsPromise,
|
|
69
|
+
s3Promise,
|
|
70
|
+
elbPromise,
|
|
71
|
+
eipPromise,
|
|
72
|
+
]);
|
|
73
|
+
(0, logger_1.success)(`Found ${ec2Opportunities.length} EC2 opportunities`);
|
|
74
|
+
(0, logger_1.success)(`Found ${ebsOpportunities.length} EBS opportunities`);
|
|
75
|
+
(0, logger_1.success)(`Found ${rdsOpportunities.length} RDS opportunities`);
|
|
76
|
+
(0, logger_1.success)(`Found ${s3Opportunities.length} S3 opportunities`);
|
|
77
|
+
(0, logger_1.success)(`Found ${elbOpportunities.length} ELB opportunities`);
|
|
78
|
+
(0, logger_1.success)(`Found ${eipOpportunities.length} EIP opportunities`);
|
|
79
|
+
// Combine opportunities
|
|
80
|
+
const allOpportunities = [
|
|
81
|
+
...ec2Opportunities,
|
|
82
|
+
...ebsOpportunities,
|
|
83
|
+
...rdsOpportunities,
|
|
84
|
+
...s3Opportunities,
|
|
85
|
+
...elbOpportunities,
|
|
86
|
+
...eipOpportunities,
|
|
87
|
+
];
|
|
88
|
+
// Filter by minimum savings if specified
|
|
89
|
+
const minSavings = options.minSavings ? parseFloat(options.minSavings) : 0;
|
|
90
|
+
const filteredOpportunities = allOpportunities.filter((opp) => opp.estimatedSavings >= minSavings);
|
|
91
|
+
// Calculate totals
|
|
92
|
+
const totalPotentialSavings = filteredOpportunities.reduce((sum, opp) => sum + opp.estimatedSavings, 0);
|
|
93
|
+
const summary = {
|
|
94
|
+
totalResources: filteredOpportunities.length,
|
|
95
|
+
idleResources: filteredOpportunities.filter((o) => o.category === 'idle').length,
|
|
96
|
+
oversizedResources: filteredOpportunities.filter((o) => o.category === 'oversized').length,
|
|
97
|
+
unusedResources: filteredOpportunities.filter((o) => o.category === 'unused').length,
|
|
98
|
+
};
|
|
99
|
+
const report = {
|
|
100
|
+
provider: 'aws',
|
|
101
|
+
accountId: 'N/A', // Will fetch from STS in future
|
|
102
|
+
region: client.region,
|
|
103
|
+
scanPeriod: {
|
|
104
|
+
start: new Date(Date.now() - (parseInt(options.days || '30') * 24 * 60 * 60 * 1000)),
|
|
105
|
+
end: new Date(),
|
|
106
|
+
},
|
|
107
|
+
opportunities: filteredOpportunities,
|
|
108
|
+
totalPotentialSavings,
|
|
109
|
+
summary,
|
|
110
|
+
};
|
|
111
|
+
// Render output
|
|
112
|
+
const topN = parseInt(options.top || '5');
|
|
113
|
+
if (options.output === 'json') {
|
|
114
|
+
(0, json_1.renderJSON)(report);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
(0, table_1.renderTable)(report, topN);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function scanAzure(options) {
|
|
121
|
+
const client = new client_2.AzureClient({
|
|
122
|
+
subscriptionId: options.subscriptionId,
|
|
123
|
+
location: options.location,
|
|
124
|
+
});
|
|
125
|
+
(0, logger_1.info)(`Scanning Azure subscription (${client.subscriptionId})...`);
|
|
126
|
+
if (client.location) {
|
|
127
|
+
(0, logger_1.info)(`Filtering resources by location: ${client.location}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
(0, logger_1.info)('Scanning all locations (no filter specified)');
|
|
131
|
+
}
|
|
132
|
+
if (options.accurate) {
|
|
133
|
+
(0, logger_1.info)('Note: --accurate flag is not yet implemented. Using estimated pricing.');
|
|
134
|
+
}
|
|
135
|
+
// Run analyzers in parallel
|
|
136
|
+
(0, logger_1.info)('Analyzing Virtual Machines...');
|
|
137
|
+
const vmPromise = (0, vms_1.analyzeAzureVMs)(client);
|
|
138
|
+
(0, logger_1.info)('Analyzing Managed Disks...');
|
|
139
|
+
const diskPromise = (0, disks_1.analyzeAzureDisks)(client);
|
|
140
|
+
(0, logger_1.info)('Analyzing Storage Accounts...');
|
|
141
|
+
const storagePromise = (0, storage_1.analyzeAzureStorage)(client);
|
|
142
|
+
(0, logger_1.info)('Analyzing SQL Databases...');
|
|
143
|
+
const sqlPromise = (0, sql_1.analyzeAzureSQL)(client);
|
|
144
|
+
(0, logger_1.info)('Analyzing Public IP Addresses...');
|
|
145
|
+
const ipPromise = (0, public_ips_1.analyzeAzurePublicIPs)(client);
|
|
146
|
+
// Wait for all analyzers to complete
|
|
147
|
+
const [vmOpportunities, diskOpportunities, storageOpportunities, sqlOpportunities, ipOpportunities,] = await Promise.all([
|
|
148
|
+
vmPromise,
|
|
149
|
+
diskPromise,
|
|
150
|
+
storagePromise,
|
|
151
|
+
sqlPromise,
|
|
152
|
+
ipPromise,
|
|
153
|
+
]);
|
|
154
|
+
(0, logger_1.success)(`Found ${vmOpportunities.length} VM opportunities`);
|
|
155
|
+
(0, logger_1.success)(`Found ${diskOpportunities.length} Disk opportunities`);
|
|
156
|
+
(0, logger_1.success)(`Found ${storageOpportunities.length} Storage opportunities`);
|
|
157
|
+
(0, logger_1.success)(`Found ${sqlOpportunities.length} SQL opportunities`);
|
|
158
|
+
(0, logger_1.success)(`Found ${ipOpportunities.length} Public IP opportunities`);
|
|
159
|
+
// Combine opportunities
|
|
160
|
+
const allOpportunities = [
|
|
161
|
+
...vmOpportunities,
|
|
162
|
+
...diskOpportunities,
|
|
163
|
+
...storageOpportunities,
|
|
164
|
+
...sqlOpportunities,
|
|
165
|
+
...ipOpportunities,
|
|
166
|
+
];
|
|
167
|
+
// Filter by minimum savings if specified
|
|
168
|
+
const minSavings = options.minSavings ? parseFloat(options.minSavings) : 0;
|
|
169
|
+
const filteredOpportunities = allOpportunities.filter((opp) => opp.estimatedSavings >= minSavings);
|
|
170
|
+
// Calculate totals
|
|
171
|
+
const totalPotentialSavings = filteredOpportunities.reduce((sum, opp) => sum + opp.estimatedSavings, 0);
|
|
172
|
+
const summary = {
|
|
173
|
+
totalResources: filteredOpportunities.length,
|
|
174
|
+
idleResources: filteredOpportunities.filter((o) => o.category === 'idle').length,
|
|
175
|
+
oversizedResources: filteredOpportunities.filter((o) => o.category === 'oversized').length,
|
|
176
|
+
unusedResources: filteredOpportunities.filter((o) => o.category === 'unused').length,
|
|
177
|
+
};
|
|
178
|
+
const report = {
|
|
179
|
+
provider: 'azure',
|
|
180
|
+
accountId: client.subscriptionId,
|
|
181
|
+
region: client.location || 'all',
|
|
182
|
+
scanPeriod: {
|
|
183
|
+
start: new Date(Date.now() - (parseInt(options.days || '7') * 24 * 60 * 60 * 1000)),
|
|
184
|
+
end: new Date(),
|
|
185
|
+
},
|
|
186
|
+
opportunities: filteredOpportunities,
|
|
187
|
+
totalPotentialSavings,
|
|
188
|
+
summary,
|
|
189
|
+
};
|
|
190
|
+
// Render output
|
|
191
|
+
const topN = parseInt(options.top || '5');
|
|
192
|
+
if (options.output === 'json') {
|
|
193
|
+
(0, json_1.renderJSON)(report);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
(0, table_1.renderTable)(report, topN);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -18,6 +18,7 @@ export declare class AWSClient {
|
|
|
18
18
|
region: string;
|
|
19
19
|
profile: string;
|
|
20
20
|
constructor(options: AWSClientOptions);
|
|
21
|
+
private getProfileRegion;
|
|
21
22
|
getEC2Client(): EC2Client;
|
|
22
23
|
getCloudWatchClient(): CloudWatchClient;
|
|
23
24
|
getCostExplorerClient(): CostExplorerClient;
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.AWSClient = void 0;
|
|
4
37
|
const client_ec2_1 = require("@aws-sdk/client-ec2");
|
|
@@ -8,6 +41,9 @@ const client_rds_1 = require("@aws-sdk/client-rds");
|
|
|
8
41
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
9
42
|
const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
10
43
|
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const ini = __importStar(require("ini"));
|
|
11
47
|
class AWSClient {
|
|
12
48
|
ec2;
|
|
13
49
|
cloudwatch;
|
|
@@ -18,8 +54,9 @@ class AWSClient {
|
|
|
18
54
|
region;
|
|
19
55
|
profile;
|
|
20
56
|
constructor(options) {
|
|
21
|
-
this.region = options.region || 'us-east-1';
|
|
22
57
|
this.profile = options.profile || 'default';
|
|
58
|
+
// Determine region: CLI option > profile config > default
|
|
59
|
+
this.region = options.region || this.getProfileRegion(this.profile) || 'us-east-1';
|
|
23
60
|
// Use environment credentials if available, otherwise fall back to profile
|
|
24
61
|
const credentials = process.env.AWS_ACCESS_KEY_ID
|
|
25
62
|
? (0, credential_providers_1.fromEnv)()
|
|
@@ -31,6 +68,28 @@ class AWSClient {
|
|
|
31
68
|
this.s3 = new client_s3_1.S3Client({ region: this.region, credentials });
|
|
32
69
|
this.elb = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client({ region: this.region, credentials });
|
|
33
70
|
}
|
|
71
|
+
getProfileRegion(profileName) {
|
|
72
|
+
try {
|
|
73
|
+
const configPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.aws', 'config');
|
|
74
|
+
if (!fs.existsSync(configPath)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
78
|
+
const config = ini.parse(configContent);
|
|
79
|
+
// Profile names in config are prefixed with "profile " except for "default"
|
|
80
|
+
const profileKey = profileName === 'default' ? 'default' : `profile ${profileName}`;
|
|
81
|
+
const profileConfig = config[profileKey];
|
|
82
|
+
// Handle both regular objects and null prototype objects
|
|
83
|
+
if (profileConfig && typeof profileConfig === 'object') {
|
|
84
|
+
return profileConfig.region || null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// If we can't read the config, just return null
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
34
93
|
getEC2Client() {
|
|
35
94
|
return this.ec2;
|
|
36
95
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ComputeManagementClient } from '@azure/arm-compute';
|
|
2
|
+
import { StorageManagementClient } from '@azure/arm-storage';
|
|
3
|
+
import { SqlManagementClient } from '@azure/arm-sql';
|
|
4
|
+
import { NetworkManagementClient } from '@azure/arm-network';
|
|
5
|
+
import { MonitorClient } from '@azure/arm-monitor';
|
|
6
|
+
export interface AzureClientConfig {
|
|
7
|
+
subscriptionId?: string;
|
|
8
|
+
location?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class AzureClient {
|
|
11
|
+
private credential;
|
|
12
|
+
subscriptionId: string;
|
|
13
|
+
location: string;
|
|
14
|
+
constructor(config?: AzureClientConfig);
|
|
15
|
+
getComputeClient(): ComputeManagementClient;
|
|
16
|
+
getStorageClient(): StorageManagementClient;
|
|
17
|
+
getSqlClient(): SqlManagementClient;
|
|
18
|
+
getNetworkClient(): NetworkManagementClient;
|
|
19
|
+
getMonitorClient(): MonitorClient;
|
|
20
|
+
}
|