aws-cost-calculator-cli 1.7.0__tar.gz → 1.8.3__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.
Files changed (35) hide show
  1. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/CHANGES.md +19 -0
  2. {aws_cost_calculator_cli-1.7.0/aws_cost_calculator_cli.egg-info → aws_cost_calculator_cli-1.8.3}/PKG-INFO +1 -1
  3. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3/aws_cost_calculator_cli.egg-info}/PKG-INFO +1 -1
  4. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/aws_cost_calculator_cli.egg-info/SOURCES.txt +1 -13
  5. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/aws_cost_calculator_cli.egg-info/top_level.txt +0 -1
  6. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/cost_calculator/api_client.py +2 -1
  7. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/cost_calculator/cli.py +206 -1
  8. aws_cost_calculator_cli-1.8.3/cost_calculator/forensics.py +322 -0
  9. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/setup.py +2 -2
  10. aws_cost_calculator_cli-1.7.0/backend/__init__.py +0 -1
  11. aws_cost_calculator_cli-1.7.0/backend/algorithms/__init__.py +0 -1
  12. aws_cost_calculator_cli-1.7.0/backend/algorithms/analyze.py +0 -272
  13. aws_cost_calculator_cli-1.7.0/backend/algorithms/cur.py +0 -189
  14. aws_cost_calculator_cli-1.7.0/backend/handlers/__init__.py +0 -1
  15. aws_cost_calculator_cli-1.7.0/backend/handlers/analyze.py +0 -112
  16. aws_cost_calculator_cli-1.7.0/backend/handlers/drill.py +0 -142
  17. aws_cost_calculator_cli-1.7.0/backend/handlers/monthly.py +0 -106
  18. aws_cost_calculator_cli-1.7.0/backend/handlers/profiles.py +0 -148
  19. aws_cost_calculator_cli-1.7.0/backend/handlers/trends.py +0 -106
  20. aws_cost_calculator_cli-1.7.0/cost_calculator/drill.py +0 -323
  21. aws_cost_calculator_cli-1.7.0/cost_calculator/monthly.py +0 -242
  22. aws_cost_calculator_cli-1.7.0/cost_calculator/trends.py +0 -353
  23. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/LICENSE +0 -0
  24. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/MANIFEST.in +0 -0
  25. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/README.md +0 -0
  26. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/aws_cost_calculator_cli.egg-info/dependency_links.txt +0 -0
  27. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/aws_cost_calculator_cli.egg-info/entry_points.txt +0 -0
  28. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/aws_cost_calculator_cli.egg-info/requires.txt +0 -0
  29. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/cost_calculator/__init__.py +0 -0
  30. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/cost_calculator/cur.py +0 -0
  31. {aws_cost_calculator_cli-1.7.0/backend/algorithms → aws_cost_calculator_cli-1.8.3/cost_calculator}/drill.py +0 -0
  32. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/cost_calculator/executor.py +0 -0
  33. {aws_cost_calculator_cli-1.7.0/backend/algorithms → aws_cost_calculator_cli-1.8.3/cost_calculator}/monthly.py +0 -0
  34. {aws_cost_calculator_cli-1.7.0/backend/algorithms → aws_cost_calculator_cli-1.8.3/cost_calculator}/trends.py +0 -0
  35. {aws_cost_calculator_cli-1.7.0 → aws_cost_calculator_cli-1.8.3}/setup.cfg +0 -0
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 1.8.0 (2025-11-07)
4
+
5
+ ### Added
6
+ - **New `cc investigate` command** - Multi-stage cost investigation tool
7
+ - Combines cost analysis, resource inventory, and CloudTrail events
8
+ - Automatically finds SSO profiles for accounts
9
+ - Generates comprehensive markdown reports
10
+ - Supports `--no-cloudtrail` flag for faster execution
11
+ - Example: `cc investigate --profile myprofile --account 123456789012`
12
+
13
+ ### Changed
14
+ - **Package cleanup** - Excluded backend, tests, and notebooks from PyPI distribution
15
+ - Backend code is only for Lambda deployment, not needed in CLI package
16
+ - Reduces package size and eliminates unnecessary files
17
+
18
+ ### Security
19
+ - Verified no sensitive data (company names, account IDs) in PyPI package
20
+ - All example data uses generic placeholders
21
+
3
22
  ## Version 1.7.0 (2025-11-06)
4
23
 
5
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.7.0
3
+ Version: 1.8.3
4
4
  Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
5
5
  Home-page: https://github.com/trilogy-group/aws-cost-calculator
6
6
  Author: Cost Optimization Team
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.7.0
3
+ Version: 1.8.3
4
4
  Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
5
5
  Home-page: https://github.com/trilogy-group/aws-cost-calculator
6
6
  Author: Cost Optimization Team
@@ -9,24 +9,12 @@ aws_cost_calculator_cli.egg-info/dependency_links.txt
9
9
  aws_cost_calculator_cli.egg-info/entry_points.txt
10
10
  aws_cost_calculator_cli.egg-info/requires.txt
11
11
  aws_cost_calculator_cli.egg-info/top_level.txt
12
- backend/__init__.py
13
- backend/algorithms/__init__.py
14
- backend/algorithms/analyze.py
15
- backend/algorithms/cur.py
16
- backend/algorithms/drill.py
17
- backend/algorithms/monthly.py
18
- backend/algorithms/trends.py
19
- backend/handlers/__init__.py
20
- backend/handlers/analyze.py
21
- backend/handlers/drill.py
22
- backend/handlers/monthly.py
23
- backend/handlers/profiles.py
24
- backend/handlers/trends.py
25
12
  cost_calculator/__init__.py
26
13
  cost_calculator/api_client.py
27
14
  cost_calculator/cli.py
28
15
  cost_calculator/cur.py
29
16
  cost_calculator/drill.py
30
17
  cost_calculator/executor.py
18
+ cost_calculator/forensics.py
31
19
  cost_calculator/monthly.py
32
20
  cost_calculator/trends.py
@@ -50,7 +50,8 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
50
50
  'monthly': 'https://6aueebodw6q4zdeu3aaexb6tle0fqhhr.lambda-url.us-east-1.on.aws/',
51
51
  'drill': 'https://3ncm2gzxrsyptrhud3ua3x5lju0akvsr.lambda-url.us-east-1.on.aws/',
52
52
  'analyze': 'https://y6npmidtxwzg62nrqzkbacfs5q0edwgs.lambda-url.us-east-1.on.aws/',
53
- 'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
53
+ 'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
54
+ 'forensics': 'https://gaekfzz7sc2hwn4mjyk64sieke0vadfo.lambda-url.us-east-1.on.aws/' # Will be populated after deployment
54
55
  }
55
56
 
56
57
  url = endpoint_urls.get(endpoint)
@@ -325,7 +325,8 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
325
325
  # Calculate days in the month that the support covers
326
326
  # Support on Nov 1 covers October (31 days)
327
327
  support_month = support_month_date - timedelta(days=1) # Go back to previous month
328
- days_in_support_month = support_month.day # This gives us the last day of the month
328
+ import calendar
329
+ days_in_support_month = calendar.monthrange(support_month.year, support_month.month)[1]
329
330
 
330
331
  # Support allocation: divide by 2 (50% allocation), then by days in month
331
332
  support_per_day = (support_cost / 2) / days_in_support_month
@@ -1135,5 +1136,209 @@ def profile(operation, name, accounts, description):
1135
1136
  click.echo(result.get('message', 'Operation completed'))
1136
1137
 
1137
1138
 
1139
+ @cli.command()
1140
+ @click.option('--profile', required=True, help='Profile name')
1141
+ @click.option('--sso', help='AWS SSO profile to use')
1142
+ @click.option('--weeks', default=8, help='Number of weeks to analyze')
1143
+ @click.option('--account', help='Focus on specific account ID')
1144
+ @click.option('--service', help='Focus on specific service')
1145
+ @click.option('--no-cloudtrail', is_flag=True, help='Skip CloudTrail analysis (faster)')
1146
+ @click.option('--output', default='investigation_report.md', help='Output file path')
1147
+ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1148
+ """
1149
+ Multi-stage cost investigation:
1150
+ 1. Analyze cost trends and drill-downs
1151
+ 2. Inventory actual resources in problem accounts
1152
+ 3. Analyze CloudTrail events (optional)
1153
+ 4. Generate comprehensive report
1154
+ """
1155
+ from cost_calculator.executor import execute_trends, execute_drill, get_credentials_dict
1156
+ from cost_calculator.api_client import call_lambda_api, is_api_configured
1157
+ from cost_calculator.forensics import format_investigation_report
1158
+ from datetime import datetime, timedelta
1159
+
1160
+ click.echo("=" * 80)
1161
+ click.echo("COST INVESTIGATION")
1162
+ click.echo("=" * 80)
1163
+ click.echo(f"Profile: {profile}")
1164
+ click.echo(f"Weeks: {weeks}")
1165
+ if account:
1166
+ click.echo(f"Account: {account}")
1167
+ if service:
1168
+ click.echo(f"Service: {service}")
1169
+ click.echo("")
1170
+
1171
+ # Load profile
1172
+ config = load_profile(profile)
1173
+
1174
+ # Override with SSO if provided
1175
+ if sso:
1176
+ config['aws_profile'] = sso
1177
+
1178
+ # Step 1: Cost Analysis
1179
+ click.echo("Step 1/3: Analyzing cost trends...")
1180
+ try:
1181
+ trends_data = execute_trends(config, weeks)
1182
+ click.echo(f"✓ Found cost data for {weeks} weeks")
1183
+ except Exception as e:
1184
+ click.echo(f"✗ Error analyzing trends: {str(e)}")
1185
+ trends_data = None
1186
+
1187
+ # Step 2: Drill-down
1188
+ click.echo("\nStep 2/3: Drilling down into costs...")
1189
+ drill_data = None
1190
+ if service or account:
1191
+ try:
1192
+ drill_data = execute_drill(config, weeks, service, account, None, False)
1193
+ click.echo(f"✓ Drill-down complete")
1194
+ except Exception as e:
1195
+ click.echo(f"✗ Error in drill-down: {str(e)}")
1196
+
1197
+ # Step 3: Resource Inventory
1198
+ click.echo("\nStep 3/3: Inventorying resources...")
1199
+ inventories = []
1200
+ cloudtrail_analyses = []
1201
+
1202
+ # Determine which accounts to investigate
1203
+ accounts_to_investigate = []
1204
+ if account:
1205
+ accounts_to_investigate = [account]
1206
+ else:
1207
+ # Extract top cost accounts from trends/drill data
1208
+ # For now, we'll need the user to specify
1209
+ click.echo("⚠️ No account specified. Use --account to inventory resources.")
1210
+
1211
+ # For each account, do inventory and CloudTrail via backend API
1212
+ for acc_id in accounts_to_investigate:
1213
+ click.echo(f"\n Investigating account {acc_id}...")
1214
+
1215
+ # Get credentials (SSO or static)
1216
+ account_creds = get_credentials_dict(config)
1217
+ if not account_creds:
1218
+ click.echo(f" ⚠️ No credentials available for account")
1219
+ continue
1220
+
1221
+ # Inventory resources via backend API
1222
+ try:
1223
+ regions = ['us-west-2', 'us-east-1', 'eu-west-1']
1224
+ for region in regions:
1225
+ try:
1226
+ if is_api_configured():
1227
+ inv = call_lambda_api(
1228
+ 'forensics',
1229
+ account_creds,
1230
+ [], # accounts not needed for forensics
1231
+ operation='inventory',
1232
+ account_id=acc_id,
1233
+ region=region
1234
+ )
1235
+ else:
1236
+ # Fallback to local execution
1237
+ from cost_calculator.forensics import inventory_resources
1238
+ acc_profile = find_account_profile(acc_id)
1239
+ if not acc_profile:
1240
+ raise Exception("No SSO profile found and API not configured")
1241
+ inv = inventory_resources(acc_id, acc_profile, region)
1242
+
1243
+ if not inv.get('error'):
1244
+ inventories.append(inv)
1245
+ click.echo(f" ✓ Inventory complete for {region}")
1246
+ click.echo(f" - EC2: {len(inv['ec2_instances'])} instances")
1247
+ click.echo(f" - EFS: {len(inv['efs_file_systems'])} file systems ({inv.get('total_efs_size_gb', 0):,.0f} GB)")
1248
+ click.echo(f" - ELB: {len(inv['load_balancers'])} load balancers")
1249
+ break
1250
+ except Exception as e:
1251
+ continue
1252
+ except Exception as e:
1253
+ click.echo(f" ✗ Inventory error: {str(e)}")
1254
+
1255
+ # CloudTrail analysis via backend API
1256
+ if not no_cloudtrail:
1257
+ try:
1258
+ start_date = (datetime.now() - timedelta(days=weeks * 7)).isoformat() + 'Z'
1259
+ end_date = datetime.now().isoformat() + 'Z'
1260
+
1261
+ if is_api_configured():
1262
+ ct_analysis = call_lambda_api(
1263
+ 'forensics',
1264
+ account_creds,
1265
+ [],
1266
+ operation='cloudtrail',
1267
+ account_id=acc_id,
1268
+ start_date=start_date,
1269
+ end_date=end_date,
1270
+ region='us-west-2'
1271
+ )
1272
+ else:
1273
+ # Fallback to local execution
1274
+ from cost_calculator.forensics import analyze_cloudtrail
1275
+ acc_profile = find_account_profile(acc_id)
1276
+ if not acc_profile:
1277
+ raise Exception("No SSO profile found and API not configured")
1278
+ start_dt = datetime.now() - timedelta(days=weeks * 7)
1279
+ end_dt = datetime.now()
1280
+ ct_analysis = analyze_cloudtrail(acc_id, acc_profile, start_dt, end_dt)
1281
+
1282
+ cloudtrail_analyses.append(ct_analysis)
1283
+
1284
+ if ct_analysis.get('error'):
1285
+ click.echo(f" ⚠️ CloudTrail: {ct_analysis['error']}")
1286
+ else:
1287
+ click.echo(f" ✓ CloudTrail analysis complete")
1288
+ click.echo(f" - {len(ct_analysis['event_summary'])} event types")
1289
+ click.echo(f" - {len(ct_analysis['write_events'])} resource changes")
1290
+ except Exception as e:
1291
+ click.echo(f" ✗ CloudTrail error: {str(e)}")
1292
+
1293
+ # Generate report
1294
+ click.echo(f"\nGenerating report...")
1295
+ report = format_investigation_report(trends_data, inventories, cloudtrail_analyses if not no_cloudtrail else None)
1296
+
1297
+ # Write to file
1298
+ with open(output, 'w') as f:
1299
+ f.write(report)
1300
+
1301
+ click.echo(f"\n✓ Investigation complete!")
1302
+ click.echo(f"✓ Report saved to: {output}")
1303
+ click.echo("")
1304
+
1305
+
1306
+ def find_account_profile(account_id):
1307
+ """
1308
+ Find the SSO profile name for a given account ID
1309
+ Returns profile name or None
1310
+ """
1311
+ import subprocess
1312
+
1313
+ try:
1314
+ # Get list of profiles
1315
+ result = subprocess.run(
1316
+ ['aws', 'configure', 'list-profiles'],
1317
+ capture_output=True,
1318
+ text=True
1319
+ )
1320
+
1321
+ profiles = result.stdout.strip().split('\n')
1322
+
1323
+ # Check each profile
1324
+ for profile in profiles:
1325
+ try:
1326
+ result = subprocess.run(
1327
+ ['aws', 'sts', 'get-caller-identity', '--profile', profile],
1328
+ capture_output=True,
1329
+ text=True,
1330
+ timeout=5
1331
+ )
1332
+
1333
+ if account_id in result.stdout:
1334
+ return profile
1335
+ except:
1336
+ continue
1337
+
1338
+ return None
1339
+ except:
1340
+ return None
1341
+
1342
+
1138
1343
  if __name__ == '__main__':
1139
1344
  cli()
@@ -0,0 +1,322 @@
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
+ report.append(f"### Account {ct['account_id']} ({ct['profile']})")
293
+ report.append(f"**Period:** {ct['start_date'][:10]} to {ct['end_date'][:10]}")
294
+ report.append("")
295
+
296
+ if ct.get('error'):
297
+ report.append(f"⚠️ Error: {ct['error']}")
298
+ report.append("")
299
+ continue
300
+
301
+ # Write events (resource changes)
302
+ if ct['write_events']:
303
+ report.append(f"**Resource Changes:** {len(ct['write_events'])} events")
304
+ for evt in ct['write_events'][:10]:
305
+ report.append(f"- `{evt['time'][:19]}` - **{evt['event_name']}**")
306
+ report.append(f" - User: {evt['username']}")
307
+ if evt['resources']:
308
+ for res in evt['resources']:
309
+ report.append(f" - Resource: {res['type']} - {res['name']}")
310
+ report.append("")
311
+
312
+ # Event summary
313
+ if ct['event_summary']:
314
+ report.append("**Top Events:**")
315
+ for event_name, count in list(ct['event_summary'].items())[:15]:
316
+ report.append(f"- {event_name}: {count}")
317
+ report.append("")
318
+
319
+ report.append("---")
320
+ report.append("")
321
+
322
+ return "\n".join(report)
@@ -5,8 +5,8 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name='aws-cost-calculator-cli',
8
- version='1.7.0',
9
- packages=find_packages(),
8
+ version='1.8.3',
9
+ packages=['cost_calculator'],
10
10
  install_requires=[
11
11
  'click>=8.0.0',
12
12
  'boto3>=1.26.0',
@@ -1 +0,0 @@
1
- # Backend package
@@ -1 +0,0 @@
1
- # Algorithms package