aws-cost-calculator-cli 1.5.2__py3-none-any.whl → 2.4.0__py3-none-any.whl
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.
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/METADATA +169 -24
- aws_cost_calculator_cli-2.4.0.dist-info/RECORD +16 -0
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/WHEEL +1 -1
- cost_calculator/api_client.py +18 -20
- cost_calculator/cli.py +1871 -242
- cost_calculator/cur.py +244 -0
- cost_calculator/dimensions.py +141 -0
- cost_calculator/executor.py +124 -101
- cost_calculator/forensics.py +323 -0
- aws_cost_calculator_cli-1.5.2.dist-info/RECORD +0 -13
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cost forensics module - Resource inventory and CloudTrail analysis
|
|
3
|
+
"""
|
|
4
|
+
import boto3
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def inventory_resources(account_id, profile, region='us-west-2'):
|
|
11
|
+
"""
|
|
12
|
+
Inventory AWS resources in an account
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
account_id: AWS account ID
|
|
16
|
+
profile: AWS profile name (SSO)
|
|
17
|
+
region: AWS region
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict with resource inventory
|
|
21
|
+
"""
|
|
22
|
+
session = boto3.Session(profile_name=profile)
|
|
23
|
+
inventory = {
|
|
24
|
+
'account_id': account_id,
|
|
25
|
+
'profile': profile,
|
|
26
|
+
'region': region,
|
|
27
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
28
|
+
'ec2_instances': [],
|
|
29
|
+
'efs_file_systems': [],
|
|
30
|
+
'load_balancers': [],
|
|
31
|
+
'dynamodb_tables': []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# EC2 Instances
|
|
36
|
+
ec2_client = session.client('ec2', region_name=region)
|
|
37
|
+
instances_response = ec2_client.describe_instances()
|
|
38
|
+
|
|
39
|
+
for reservation in instances_response['Reservations']:
|
|
40
|
+
for instance in reservation['Instances']:
|
|
41
|
+
if instance['State']['Name'] == 'running':
|
|
42
|
+
name = 'N/A'
|
|
43
|
+
for tag in instance.get('Tags', []):
|
|
44
|
+
if tag['Key'] == 'Name':
|
|
45
|
+
name = tag['Value']
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
inventory['ec2_instances'].append({
|
|
49
|
+
'instance_id': instance['InstanceId'],
|
|
50
|
+
'instance_type': instance['InstanceType'],
|
|
51
|
+
'name': name,
|
|
52
|
+
'state': instance['State']['Name'],
|
|
53
|
+
'launch_time': instance['LaunchTime'].isoformat(),
|
|
54
|
+
'availability_zone': instance['Placement']['AvailabilityZone']
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# EFS File Systems
|
|
58
|
+
efs_client = session.client('efs', region_name=region)
|
|
59
|
+
efs_response = efs_client.describe_file_systems()
|
|
60
|
+
|
|
61
|
+
total_efs_size = 0
|
|
62
|
+
for fs in efs_response['FileSystems']:
|
|
63
|
+
size_bytes = fs['SizeInBytes']['Value']
|
|
64
|
+
size_gb = size_bytes / (1024**3)
|
|
65
|
+
total_efs_size += size_gb
|
|
66
|
+
|
|
67
|
+
inventory['efs_file_systems'].append({
|
|
68
|
+
'file_system_id': fs['FileSystemId'],
|
|
69
|
+
'name': fs.get('Name', 'N/A'),
|
|
70
|
+
'size_gb': round(size_gb, 2),
|
|
71
|
+
'creation_time': fs['CreationTime'].isoformat(),
|
|
72
|
+
'number_of_mount_targets': fs['NumberOfMountTargets']
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
inventory['total_efs_size_gb'] = round(total_efs_size, 2)
|
|
76
|
+
|
|
77
|
+
# Load Balancers
|
|
78
|
+
elbv2_client = session.client('elbv2', region_name=region)
|
|
79
|
+
elb_response = elbv2_client.describe_load_balancers()
|
|
80
|
+
|
|
81
|
+
for lb in elb_response['LoadBalancers']:
|
|
82
|
+
inventory['load_balancers'].append({
|
|
83
|
+
'name': lb['LoadBalancerName'],
|
|
84
|
+
'type': lb['Type'],
|
|
85
|
+
'dns_name': lb['DNSName'],
|
|
86
|
+
'scheme': lb['Scheme'],
|
|
87
|
+
'created_time': lb['CreatedTime'].isoformat(),
|
|
88
|
+
'availability_zones': [az['ZoneName'] for az in lb['AvailabilityZones']]
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
# DynamoDB Tables (only if region supports it)
|
|
92
|
+
try:
|
|
93
|
+
ddb_client = session.client('dynamodb', region_name=region)
|
|
94
|
+
tables_response = ddb_client.list_tables()
|
|
95
|
+
|
|
96
|
+
for table_name in tables_response['TableNames'][:20]: # Limit to 20 tables
|
|
97
|
+
table_desc = ddb_client.describe_table(TableName=table_name)
|
|
98
|
+
table_info = table_desc['Table']
|
|
99
|
+
|
|
100
|
+
# Get backup settings
|
|
101
|
+
try:
|
|
102
|
+
backup_desc = ddb_client.describe_continuous_backups(TableName=table_name)
|
|
103
|
+
pitr_status = backup_desc['ContinuousBackupsDescription']['PointInTimeRecoveryDescription']['PointInTimeRecoveryStatus']
|
|
104
|
+
except:
|
|
105
|
+
pitr_status = 'UNKNOWN'
|
|
106
|
+
|
|
107
|
+
size_gb = table_info.get('TableSizeBytes', 0) / (1024**3)
|
|
108
|
+
|
|
109
|
+
inventory['dynamodb_tables'].append({
|
|
110
|
+
'table_name': table_name,
|
|
111
|
+
'size_gb': round(size_gb, 2),
|
|
112
|
+
'item_count': table_info.get('ItemCount', 0),
|
|
113
|
+
'pitr_status': pitr_status,
|
|
114
|
+
'created_time': table_info['CreationDateTime'].isoformat()
|
|
115
|
+
})
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# DynamoDB might not be available in all regions
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
inventory['error'] = str(e)
|
|
122
|
+
|
|
123
|
+
return inventory
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def analyze_cloudtrail(account_id, profile, start_date, end_date, region='us-west-2'):
|
|
127
|
+
"""
|
|
128
|
+
Analyze CloudTrail events for an account
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
account_id: AWS account ID
|
|
132
|
+
profile: AWS profile name (SSO)
|
|
133
|
+
start_date: Start datetime
|
|
134
|
+
end_date: End datetime
|
|
135
|
+
region: AWS region
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict with CloudTrail event summary
|
|
139
|
+
"""
|
|
140
|
+
session = boto3.Session(profile_name=profile)
|
|
141
|
+
ct_client = session.client('cloudtrail', region_name=region)
|
|
142
|
+
|
|
143
|
+
analysis = {
|
|
144
|
+
'account_id': account_id,
|
|
145
|
+
'profile': profile,
|
|
146
|
+
'region': region,
|
|
147
|
+
'start_date': start_date.isoformat(),
|
|
148
|
+
'end_date': end_date.isoformat(),
|
|
149
|
+
'event_summary': {},
|
|
150
|
+
'write_events': [],
|
|
151
|
+
'error': None
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Events that indicate resource creation/modification
|
|
155
|
+
write_event_names = [
|
|
156
|
+
'RunInstances', 'CreateVolume', 'AttachVolume',
|
|
157
|
+
'CreateFileSystem', 'ModifyFileSystem',
|
|
158
|
+
'CreateLoadBalancer', 'ModifyLoadBalancerAttributes',
|
|
159
|
+
'CreateTable', 'UpdateTable', 'UpdateContinuousBackups',
|
|
160
|
+
'CreateBackupVault', 'StartBackupJob'
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
event_counts = defaultdict(int)
|
|
165
|
+
|
|
166
|
+
# Query CloudTrail
|
|
167
|
+
paginator = ct_client.get_paginator('lookup_events')
|
|
168
|
+
|
|
169
|
+
for page in paginator.paginate(
|
|
170
|
+
StartTime=start_date,
|
|
171
|
+
EndTime=end_date,
|
|
172
|
+
MaxResults=50,
|
|
173
|
+
PaginationConfig={'MaxItems': 200}
|
|
174
|
+
):
|
|
175
|
+
for event in page.get('Events', []):
|
|
176
|
+
event_name = event.get('EventName', '')
|
|
177
|
+
event_counts[event_name] += 1
|
|
178
|
+
|
|
179
|
+
# Capture write events
|
|
180
|
+
if event_name in write_event_names:
|
|
181
|
+
event_detail = json.loads(event['CloudTrailEvent'])
|
|
182
|
+
|
|
183
|
+
analysis['write_events'].append({
|
|
184
|
+
'time': event.get('EventTime').isoformat(),
|
|
185
|
+
'event_name': event_name,
|
|
186
|
+
'username': event.get('Username', 'N/A'),
|
|
187
|
+
'resources': [
|
|
188
|
+
{
|
|
189
|
+
'type': r.get('ResourceType', 'N/A'),
|
|
190
|
+
'name': r.get('ResourceName', 'N/A')
|
|
191
|
+
}
|
|
192
|
+
for r in event.get('Resources', [])[:3]
|
|
193
|
+
]
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
# Convert to regular dict and sort
|
|
197
|
+
analysis['event_summary'] = dict(sorted(
|
|
198
|
+
event_counts.items(),
|
|
199
|
+
key=lambda x: x[1],
|
|
200
|
+
reverse=True
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
analysis['error'] = str(e)
|
|
205
|
+
|
|
206
|
+
return analysis
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def format_investigation_report(cost_data, inventories, cloudtrail_data=None):
|
|
210
|
+
"""
|
|
211
|
+
Format investigation data into markdown report
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
cost_data: Cost analysis results from trends/drill
|
|
215
|
+
inventories: List of resource inventories
|
|
216
|
+
cloudtrail_data: List of CloudTrail analyses (optional)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
str: Markdown formatted report
|
|
220
|
+
"""
|
|
221
|
+
report = []
|
|
222
|
+
report.append("# Cost Investigation Report")
|
|
223
|
+
report.append(f"**Generated:** {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
224
|
+
report.append("")
|
|
225
|
+
|
|
226
|
+
# Cost Analysis Section
|
|
227
|
+
if cost_data:
|
|
228
|
+
report.append("## Cost Analysis")
|
|
229
|
+
report.append("")
|
|
230
|
+
# Add cost data formatting here
|
|
231
|
+
# This will be populated from trends/drill results
|
|
232
|
+
|
|
233
|
+
# Resource Inventory Section
|
|
234
|
+
if inventories:
|
|
235
|
+
report.append("## Resource Inventory")
|
|
236
|
+
report.append("")
|
|
237
|
+
|
|
238
|
+
for inv in inventories:
|
|
239
|
+
profile_name = inv.get('profile', inv['account_id'])
|
|
240
|
+
report.append(f"### Account {inv['account_id']} ({profile_name})")
|
|
241
|
+
report.append(f"**Region:** {inv['region']}")
|
|
242
|
+
report.append("")
|
|
243
|
+
|
|
244
|
+
# EC2 Instances
|
|
245
|
+
if inv['ec2_instances']:
|
|
246
|
+
report.append(f"**EC2 Instances:** {len(inv['ec2_instances'])} running")
|
|
247
|
+
for instance in inv['ec2_instances'][:10]: # Show first 10
|
|
248
|
+
report.append(f"- `{instance['instance_id']}`: {instance['instance_type']} ({instance['name']})")
|
|
249
|
+
report.append(f" - Launched: {instance['launch_time'][:10]}, AZ: {instance['availability_zone']}")
|
|
250
|
+
if len(inv['ec2_instances']) > 10:
|
|
251
|
+
report.append(f" ... and {len(inv['ec2_instances']) - 10} more")
|
|
252
|
+
report.append("")
|
|
253
|
+
|
|
254
|
+
# EFS File Systems
|
|
255
|
+
if inv['efs_file_systems']:
|
|
256
|
+
total_size = inv.get('total_efs_size_gb', 0)
|
|
257
|
+
report.append(f"**EFS File Systems:** {len(inv['efs_file_systems'])} total, {total_size:,.0f} GB")
|
|
258
|
+
for fs in inv['efs_file_systems']:
|
|
259
|
+
report.append(f"- `{fs['file_system_id']}` ({fs['name']}): {fs['size_gb']:,.2f} GB")
|
|
260
|
+
report.append(f" - Created: {fs['creation_time'][:10]}")
|
|
261
|
+
report.append("")
|
|
262
|
+
|
|
263
|
+
# Load Balancers
|
|
264
|
+
if inv['load_balancers']:
|
|
265
|
+
report.append(f"**Load Balancers:** {len(inv['load_balancers'])}")
|
|
266
|
+
for lb in inv['load_balancers'][:10]: # Show first 10
|
|
267
|
+
report.append(f"- `{lb['name']}`: {lb['type']}")
|
|
268
|
+
report.append(f" - Created: {lb['created_time'][:10]}, Scheme: {lb['scheme']}")
|
|
269
|
+
if len(inv['load_balancers']) > 10:
|
|
270
|
+
report.append(f" ... and {len(inv['load_balancers']) - 10} more")
|
|
271
|
+
report.append("")
|
|
272
|
+
|
|
273
|
+
# DynamoDB Tables
|
|
274
|
+
if inv['dynamodb_tables']:
|
|
275
|
+
report.append(f"**DynamoDB Tables:** {len(inv['dynamodb_tables'])}")
|
|
276
|
+
for table in inv['dynamodb_tables'][:10]:
|
|
277
|
+
report.append(f"- `{table['table_name']}`: {table['size_gb']:.2f} GB, {table['item_count']:,} items")
|
|
278
|
+
report.append(f" - PITR: {table['pitr_status']}, Created: {table['created_time'][:10]}")
|
|
279
|
+
if len(inv['dynamodb_tables']) > 10:
|
|
280
|
+
report.append(f" ... and {len(inv['dynamodb_tables']) - 10} more")
|
|
281
|
+
report.append("")
|
|
282
|
+
|
|
283
|
+
report.append("---")
|
|
284
|
+
report.append("")
|
|
285
|
+
|
|
286
|
+
# CloudTrail Section
|
|
287
|
+
if cloudtrail_data:
|
|
288
|
+
report.append("## CloudTrail Events")
|
|
289
|
+
report.append("")
|
|
290
|
+
|
|
291
|
+
for ct in cloudtrail_data:
|
|
292
|
+
profile_name = ct.get('profile', ct['account_id'])
|
|
293
|
+
report.append(f"### Account {ct['account_id']} ({profile_name})")
|
|
294
|
+
report.append(f"**Period:** {ct['start_date'][:10]} to {ct['end_date'][:10]}")
|
|
295
|
+
report.append("")
|
|
296
|
+
|
|
297
|
+
if ct.get('error'):
|
|
298
|
+
report.append(f"⚠️ Error: {ct['error']}")
|
|
299
|
+
report.append("")
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Write events (resource changes)
|
|
303
|
+
if ct['write_events']:
|
|
304
|
+
report.append(f"**Resource Changes:** {len(ct['write_events'])} events")
|
|
305
|
+
for evt in ct['write_events'][:10]:
|
|
306
|
+
report.append(f"- `{evt['time'][:19]}` - **{evt['event_name']}**")
|
|
307
|
+
report.append(f" - User: {evt['username']}")
|
|
308
|
+
if evt['resources']:
|
|
309
|
+
for res in evt['resources']:
|
|
310
|
+
report.append(f" - Resource: {res['type']} - {res['name']}")
|
|
311
|
+
report.append("")
|
|
312
|
+
|
|
313
|
+
# Event summary
|
|
314
|
+
if ct['event_summary']:
|
|
315
|
+
report.append("**Top Events:**")
|
|
316
|
+
for event_name, count in list(ct['event_summary'].items())[:15]:
|
|
317
|
+
report.append(f"- {event_name}: {count}")
|
|
318
|
+
report.append("")
|
|
319
|
+
|
|
320
|
+
report.append("---")
|
|
321
|
+
report.append("")
|
|
322
|
+
|
|
323
|
+
return "\n".join(report)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
aws_cost_calculator_cli-1.5.2.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
-
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
-
cost_calculator/api_client.py,sha256=LUzQmveDF0X9MqAyThp9mbSzJzkOO73Pk4F7IEJjASU,2353
|
|
4
|
-
cost_calculator/cli.py,sha256=ufK28divdvrceEryWd8cCWjvG5pT2owaqprskX2epeQ,32589
|
|
5
|
-
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
6
|
-
cost_calculator/executor.py,sha256=tVyyBtXIj9OPyG-xQj8CUmyFjDhb9IVK639360dUZDc,8076
|
|
7
|
-
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
8
|
-
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
9
|
-
aws_cost_calculator_cli-1.5.2.dist-info/METADATA,sha256=0wxy-jgVC-paubGHN87mDObETGr_u9qU4ZIP3xV49hM,8176
|
|
10
|
-
aws_cost_calculator_cli-1.5.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
11
|
-
aws_cost_calculator_cli-1.5.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
12
|
-
aws_cost_calculator_cli-1.5.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
13
|
-
aws_cost_calculator_cli-1.5.2.dist-info/RECORD,,
|
{aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|