aws-cost-calculator-cli 1.6.3__py3-none-any.whl → 1.9.1__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.
Potentially problematic release.
This version of aws-cost-calculator-cli might be problematic. Click here for more details.
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/METADATA +13 -1
- aws_cost_calculator_cli-1.9.1.dist-info/RECORD +15 -0
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/WHEEL +1 -1
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/top_level.txt +0 -1
- cost_calculator/api_client.py +2 -1
- cost_calculator/cli.py +301 -5
- cost_calculator/cur.py +244 -0
- cost_calculator/executor.py +59 -92
- cost_calculator/forensics.py +323 -0
- aws_cost_calculator_cli-1.6.3.dist-info/RECORD +0 -25
- backend/__init__.py +0 -1
- backend/algorithms/__init__.py +0 -1
- backend/algorithms/analyze.py +0 -272
- backend/algorithms/drill.py +0 -323
- backend/algorithms/monthly.py +0 -242
- backend/algorithms/trends.py +0 -353
- backend/handlers/__init__.py +0 -1
- backend/handlers/analyze.py +0 -112
- backend/handlers/drill.py +0 -117
- backend/handlers/monthly.py +0 -106
- backend/handlers/profiles.py +0 -148
- backend/handlers/trends.py +0 -106
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/licenses/LICENSE +0 -0
{aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-1.9.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.1
|
|
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
|
|
@@ -229,6 +229,16 @@ cc setup-api
|
|
|
229
229
|
# Enter your API secret when prompted (input will be hidden)
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
**CUR Configuration (for --resources flag):**
|
|
233
|
+
|
|
234
|
+
To use resource-level queries, configure CUR settings:
|
|
235
|
+
```bash
|
|
236
|
+
cc setup-cur
|
|
237
|
+
# Enter: Database name, table name, S3 output location
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
This saves configuration to `~/.config/cost-calculator/cur_config.json`
|
|
241
|
+
|
|
232
242
|
Or set manually:
|
|
233
243
|
```bash
|
|
234
244
|
export COST_API_SECRET="your-api-secret"
|
|
@@ -374,11 +384,13 @@ The `drill` command allows you to investigate cost changes at different levels o
|
|
|
374
384
|
1. **Start broad:** `cc trends` → See EC2 costs up $1000
|
|
375
385
|
2. **Drill by service:** `cc drill --service "EC2 - Other"` → See which accounts
|
|
376
386
|
3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
|
|
387
|
+
4. **Resource-level:** `cc drill --service "EC2 - Other" --account 123 --resources` → See individual instance IDs
|
|
377
388
|
|
|
378
389
|
**Features:**
|
|
379
390
|
- **Week-over-week cost analysis**: Compare costs between consecutive weeks
|
|
380
391
|
- **Month-over-month cost analysis**: Compare costs between consecutive months
|
|
381
392
|
- **Drill-down analysis**: Analyze costs by service, account, or usage type
|
|
393
|
+
- **Resource-level analysis**: See individual resource IDs and costs using CUR data (NEW in v1.7.0)
|
|
382
394
|
- **Pandas aggregations**: Time series analysis with sum, avg, std across all weeks
|
|
383
395
|
- **Volatility detection**: Identify services with high cost variability and outliers
|
|
384
396
|
- **Trend detection**: Auto-detect increasing/decreasing cost patterns
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.9.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/api_client.py,sha256=4ZI2XcGIN3FBeQqb7xOxQ91kCoeM43-rExiOELXoKBQ,2485
|
|
4
|
+
cost_calculator/cli.py,sha256=IRlRxefn9rfrt6EWwSEcMi9HCISHuOfnUs2_Bf5IZkA,54085
|
|
5
|
+
cost_calculator/cur.py,sha256=QaZ_nyDSw5_cti-h5Ho6eYLbqzY5TWoub24DpyzIiSs,9502
|
|
6
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
7
|
+
cost_calculator/executor.py,sha256=vZX3BCgTRHwBfxC0WqQsHgv1ww5rpmKqLCTrW2sflSY,6509
|
|
8
|
+
cost_calculator/forensics.py,sha256=uhRo3I_zOeMEaBENHfgq65URga31W0Z4vzS2UN6VmTY,12819
|
|
9
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
10
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
11
|
+
aws_cost_calculator_cli-1.9.1.dist-info/METADATA,sha256=jMNKiSSMAbMNajhVxBOyzSuJaWg3lrGfuCBSz0THo38,11978
|
|
12
|
+
aws_cost_calculator_cli-1.9.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
13
|
+
aws_cost_calculator_cli-1.9.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
14
|
+
aws_cost_calculator_cli-1.9.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
15
|
+
aws_cost_calculator_cli-1.9.1.dist-info/RECORD,,
|
cost_calculator/api_client.py
CHANGED
|
@@ -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)
|
cost_calculator/cli.py
CHANGED
|
@@ -136,8 +136,11 @@ def load_profile(profile_name):
|
|
|
136
136
|
profile_data = response_data.get('profile', response_data)
|
|
137
137
|
profile = {'accounts': profile_data['accounts']}
|
|
138
138
|
|
|
139
|
+
# If profile has aws_profile field, use it
|
|
140
|
+
if 'aws_profile' in profile_data:
|
|
141
|
+
profile['aws_profile'] = profile_data['aws_profile']
|
|
139
142
|
# Check for AWS_PROFILE environment variable (SSO support)
|
|
140
|
-
|
|
143
|
+
elif os.environ.get('AWS_PROFILE'):
|
|
141
144
|
profile['aws_profile'] = os.environ['AWS_PROFILE']
|
|
142
145
|
# Use environment credentials
|
|
143
146
|
elif os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
@@ -146,6 +149,33 @@ def load_profile(profile_name):
|
|
|
146
149
|
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
147
150
|
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
148
151
|
}
|
|
152
|
+
else:
|
|
153
|
+
# Try to find a matching AWS profile by name
|
|
154
|
+
# This allows "khoros" profile to work with "khoros_umbrella" AWS profile
|
|
155
|
+
import subprocess
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
['aws', 'configure', 'list-profiles'],
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
timeout=5
|
|
162
|
+
)
|
|
163
|
+
if result.returncode == 0:
|
|
164
|
+
available_profiles = result.stdout.strip().split('\n')
|
|
165
|
+
# Try exact match first
|
|
166
|
+
if profile_name in available_profiles:
|
|
167
|
+
profile['aws_profile'] = profile_name
|
|
168
|
+
# Try with common suffixes
|
|
169
|
+
elif f"{profile_name}_umbrella" in available_profiles:
|
|
170
|
+
profile['aws_profile'] = f"{profile_name}_umbrella"
|
|
171
|
+
elif f"{profile_name}-umbrella" in available_profiles:
|
|
172
|
+
profile['aws_profile'] = f"{profile_name}-umbrella"
|
|
173
|
+
elif f"{profile_name}_prod" in available_profiles:
|
|
174
|
+
profile['aws_profile'] = f"{profile_name}_prod"
|
|
175
|
+
# If no match found, leave it unset - user must provide --sso
|
|
176
|
+
except:
|
|
177
|
+
# If we can't list profiles, leave it unset - user must provide --sso
|
|
178
|
+
pass
|
|
149
179
|
|
|
150
180
|
return profile
|
|
151
181
|
else:
|
|
@@ -325,7 +355,8 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
|
325
355
|
# Calculate days in the month that the support covers
|
|
326
356
|
# Support on Nov 1 covers October (31 days)
|
|
327
357
|
support_month = support_month_date - timedelta(days=1) # Go back to previous month
|
|
328
|
-
|
|
358
|
+
import calendar
|
|
359
|
+
days_in_support_month = calendar.monthrange(support_month.year, support_month.month)[1]
|
|
329
360
|
|
|
330
361
|
# Support allocation: divide by 2 (50% allocation), then by days in month
|
|
331
362
|
support_per_day = (support_cost / 2) / days_in_support_month
|
|
@@ -386,6 +417,43 @@ def cli():
|
|
|
386
417
|
pass
|
|
387
418
|
|
|
388
419
|
|
|
420
|
+
@cli.command('setup-cur')
|
|
421
|
+
@click.option('--database', required=True, prompt='CUR Athena Database', help='Athena database name for CUR')
|
|
422
|
+
@click.option('--table', required=True, prompt='CUR Table Name', help='CUR table name')
|
|
423
|
+
@click.option('--s3-output', required=True, prompt='S3 Output Location', help='S3 bucket for Athena query results')
|
|
424
|
+
def setup_cur(database, table, s3_output):
|
|
425
|
+
"""
|
|
426
|
+
Configure CUR (Cost and Usage Report) settings for resource-level queries
|
|
427
|
+
|
|
428
|
+
Saves CUR configuration to ~/.config/cost-calculator/cur_config.json
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
cc setup-cur --database my_cur_db --table cur_table --s3-output s3://my-bucket/
|
|
432
|
+
"""
|
|
433
|
+
import json
|
|
434
|
+
|
|
435
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
436
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
|
|
438
|
+
config_file = config_dir / 'cur_config.json'
|
|
439
|
+
|
|
440
|
+
config = {
|
|
441
|
+
'database': database,
|
|
442
|
+
'table': table,
|
|
443
|
+
's3_output': s3_output
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
with open(config_file, 'w') as f:
|
|
447
|
+
json.dump(config, f, indent=2)
|
|
448
|
+
|
|
449
|
+
click.echo(f"✓ CUR configuration saved to {config_file}")
|
|
450
|
+
click.echo(f" Database: {database}")
|
|
451
|
+
click.echo(f" Table: {table}")
|
|
452
|
+
click.echo(f" S3 Output: {s3_output}")
|
|
453
|
+
click.echo("")
|
|
454
|
+
click.echo("You can now use: cc drill --service 'EC2 - Other' --resources")
|
|
455
|
+
|
|
456
|
+
|
|
389
457
|
@cli.command('setup-api')
|
|
390
458
|
@click.option('--api-secret', required=True, prompt=True, hide_input=True, help='COST_API_SECRET value')
|
|
391
459
|
def setup_api(api_secret):
|
|
@@ -887,14 +955,19 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
|
|
|
887
955
|
@click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
|
|
888
956
|
@click.option('--account', help='Filter by account ID')
|
|
889
957
|
@click.option('--usage-type', help='Filter by usage type')
|
|
958
|
+
@click.option('--resources', is_flag=True, help='Show individual resource IDs (requires CUR, uses Athena)')
|
|
890
959
|
@click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
|
|
891
960
|
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
892
961
|
@click.option('--sso', help='AWS SSO profile name')
|
|
893
962
|
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
894
963
|
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
895
964
|
@click.option('--session-token', help='AWS Session Token')
|
|
896
|
-
def drill(profile, weeks, service, account, usage_type, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
897
|
-
"""
|
|
965
|
+
def drill(profile, weeks, service, account, usage_type, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
966
|
+
"""
|
|
967
|
+
Drill down into cost changes by service, account, or usage type
|
|
968
|
+
|
|
969
|
+
Add --resources flag to see individual resource IDs and costs (requires CUR data via Athena)
|
|
970
|
+
"""
|
|
898
971
|
|
|
899
972
|
# Load profile
|
|
900
973
|
config = load_profile(profile)
|
|
@@ -908,10 +981,19 @@ def drill(profile, weeks, service, account, usage_type, output, json_output, sso
|
|
|
908
981
|
click.echo(f" Account filter: {account}")
|
|
909
982
|
if usage_type:
|
|
910
983
|
click.echo(f" Usage type filter: {usage_type}")
|
|
984
|
+
if resources:
|
|
985
|
+
click.echo(f" Mode: Resource-level (CUR via Athena)")
|
|
911
986
|
click.echo("")
|
|
912
987
|
|
|
913
988
|
# Execute via API or locally
|
|
914
|
-
drill_data = execute_drill(config, weeks, service, account, usage_type)
|
|
989
|
+
drill_data = execute_drill(config, weeks, service, account, usage_type, resources)
|
|
990
|
+
|
|
991
|
+
# Handle resource-level output differently
|
|
992
|
+
if resources:
|
|
993
|
+
from cost_calculator.cur import format_resource_output
|
|
994
|
+
output_text = format_resource_output(drill_data)
|
|
995
|
+
click.echo(output_text)
|
|
996
|
+
return
|
|
915
997
|
|
|
916
998
|
if json_output:
|
|
917
999
|
# Output as JSON
|
|
@@ -1084,5 +1166,219 @@ def profile(operation, name, accounts, description):
|
|
|
1084
1166
|
click.echo(result.get('message', 'Operation completed'))
|
|
1085
1167
|
|
|
1086
1168
|
|
|
1169
|
+
@cli.command()
|
|
1170
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
1171
|
+
@click.option('--sso', help='AWS SSO profile to use')
|
|
1172
|
+
@click.option('--weeks', default=8, help='Number of weeks to analyze')
|
|
1173
|
+
@click.option('--account', help='Focus on specific account ID')
|
|
1174
|
+
@click.option('--service', help='Focus on specific service')
|
|
1175
|
+
@click.option('--no-cloudtrail', is_flag=True, help='Skip CloudTrail analysis (faster)')
|
|
1176
|
+
@click.option('--output', default='investigation_report.md', help='Output file path')
|
|
1177
|
+
def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
|
|
1178
|
+
"""
|
|
1179
|
+
Multi-stage cost investigation:
|
|
1180
|
+
1. Analyze cost trends and drill-downs
|
|
1181
|
+
2. Inventory actual resources in problem accounts
|
|
1182
|
+
3. Analyze CloudTrail events (optional)
|
|
1183
|
+
4. Generate comprehensive report
|
|
1184
|
+
"""
|
|
1185
|
+
from cost_calculator.executor import execute_trends, execute_drill, get_credentials_dict
|
|
1186
|
+
from cost_calculator.api_client import call_lambda_api, is_api_configured
|
|
1187
|
+
from cost_calculator.forensics import format_investigation_report
|
|
1188
|
+
from datetime import datetime, timedelta
|
|
1189
|
+
|
|
1190
|
+
click.echo("=" * 80)
|
|
1191
|
+
click.echo("COST INVESTIGATION")
|
|
1192
|
+
click.echo("=" * 80)
|
|
1193
|
+
click.echo(f"Profile: {profile}")
|
|
1194
|
+
click.echo(f"Weeks: {weeks}")
|
|
1195
|
+
if account:
|
|
1196
|
+
click.echo(f"Account: {account}")
|
|
1197
|
+
if service:
|
|
1198
|
+
click.echo(f"Service: {service}")
|
|
1199
|
+
click.echo("")
|
|
1200
|
+
|
|
1201
|
+
# Load profile
|
|
1202
|
+
config = load_profile(profile)
|
|
1203
|
+
|
|
1204
|
+
# Override with SSO if provided
|
|
1205
|
+
if sso:
|
|
1206
|
+
config['aws_profile'] = sso
|
|
1207
|
+
|
|
1208
|
+
# Validate that we have a way to get credentials
|
|
1209
|
+
if 'aws_profile' not in config and 'credentials' not in config:
|
|
1210
|
+
import subprocess
|
|
1211
|
+
try:
|
|
1212
|
+
result = subprocess.run(
|
|
1213
|
+
['aws', 'configure', 'list-profiles'],
|
|
1214
|
+
capture_output=True,
|
|
1215
|
+
text=True,
|
|
1216
|
+
timeout=5
|
|
1217
|
+
)
|
|
1218
|
+
available = result.stdout.strip().split('\n') if result.returncode == 0 else []
|
|
1219
|
+
suggestion = f"\nAvailable AWS profiles: {', '.join(available[:5])}" if available else ""
|
|
1220
|
+
except:
|
|
1221
|
+
suggestion = ""
|
|
1222
|
+
|
|
1223
|
+
raise click.ClickException(
|
|
1224
|
+
f"Profile '{profile}' has no AWS authentication configured.\n"
|
|
1225
|
+
f"Use --sso flag to specify your AWS SSO profile:\n"
|
|
1226
|
+
f" cc investigate --profile {profile} --sso YOUR_AWS_PROFILE{suggestion}"
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
# Step 1: Cost Analysis
|
|
1230
|
+
click.echo("Step 1/3: Analyzing cost trends...")
|
|
1231
|
+
try:
|
|
1232
|
+
trends_data = execute_trends(config, weeks)
|
|
1233
|
+
click.echo(f"✓ Found cost data for {weeks} weeks")
|
|
1234
|
+
except Exception as e:
|
|
1235
|
+
click.echo(f"✗ Error analyzing trends: {str(e)}")
|
|
1236
|
+
trends_data = None
|
|
1237
|
+
|
|
1238
|
+
# Step 2: Drill-down
|
|
1239
|
+
click.echo("\nStep 2/3: Drilling down into costs...")
|
|
1240
|
+
drill_data = None
|
|
1241
|
+
if service or account:
|
|
1242
|
+
try:
|
|
1243
|
+
drill_data = execute_drill(config, weeks, service, account, None, False)
|
|
1244
|
+
click.echo(f"✓ Drill-down complete")
|
|
1245
|
+
except Exception as e:
|
|
1246
|
+
click.echo(f"✗ Error in drill-down: {str(e)}")
|
|
1247
|
+
|
|
1248
|
+
# Step 3: Resource Inventory
|
|
1249
|
+
click.echo("\nStep 3/3: Inventorying resources...")
|
|
1250
|
+
inventories = []
|
|
1251
|
+
cloudtrail_analyses = []
|
|
1252
|
+
|
|
1253
|
+
# Determine which accounts to investigate
|
|
1254
|
+
accounts_to_investigate = []
|
|
1255
|
+
if account:
|
|
1256
|
+
accounts_to_investigate = [account]
|
|
1257
|
+
else:
|
|
1258
|
+
# Extract top cost accounts from trends/drill data
|
|
1259
|
+
# For now, we'll need the user to specify
|
|
1260
|
+
click.echo("⚠️ No account specified. Use --account to inventory resources.")
|
|
1261
|
+
|
|
1262
|
+
# For each account, do inventory and CloudTrail via backend API
|
|
1263
|
+
for acc_id in accounts_to_investigate:
|
|
1264
|
+
click.echo(f"\n Investigating account {acc_id}...")
|
|
1265
|
+
|
|
1266
|
+
# Get credentials (SSO or static)
|
|
1267
|
+
account_creds = get_credentials_dict(config)
|
|
1268
|
+
if not account_creds:
|
|
1269
|
+
click.echo(f" ⚠️ No credentials available for account")
|
|
1270
|
+
continue
|
|
1271
|
+
|
|
1272
|
+
# Inventory resources via backend API only
|
|
1273
|
+
if not is_api_configured():
|
|
1274
|
+
click.echo(f" ✗ API not configured. Set COST_API_SECRET environment variable.")
|
|
1275
|
+
continue
|
|
1276
|
+
|
|
1277
|
+
try:
|
|
1278
|
+
regions = ['us-west-2', 'us-east-1', 'eu-west-1']
|
|
1279
|
+
for region in regions:
|
|
1280
|
+
try:
|
|
1281
|
+
inv = call_lambda_api(
|
|
1282
|
+
'forensics',
|
|
1283
|
+
account_creds,
|
|
1284
|
+
[], # accounts not needed for forensics
|
|
1285
|
+
operation='inventory',
|
|
1286
|
+
account_id=acc_id,
|
|
1287
|
+
region=region
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
if not inv.get('error'):
|
|
1291
|
+
inventories.append(inv)
|
|
1292
|
+
click.echo(f" ✓ Inventory complete for {region}")
|
|
1293
|
+
click.echo(f" - EC2: {len(inv['ec2_instances'])} instances")
|
|
1294
|
+
click.echo(f" - EFS: {len(inv['efs_file_systems'])} file systems ({inv.get('total_efs_size_gb', 0):,.0f} GB)")
|
|
1295
|
+
click.echo(f" - ELB: {len(inv['load_balancers'])} load balancers")
|
|
1296
|
+
break
|
|
1297
|
+
except Exception as e:
|
|
1298
|
+
continue
|
|
1299
|
+
except Exception as e:
|
|
1300
|
+
click.echo(f" ✗ Inventory error: {str(e)}")
|
|
1301
|
+
|
|
1302
|
+
# CloudTrail analysis via backend API only
|
|
1303
|
+
if not no_cloudtrail:
|
|
1304
|
+
if not is_api_configured():
|
|
1305
|
+
click.echo(f" ✗ CloudTrail skipped: API not configured")
|
|
1306
|
+
else:
|
|
1307
|
+
try:
|
|
1308
|
+
start_date = (datetime.now() - timedelta(days=weeks * 7)).isoformat() + 'Z'
|
|
1309
|
+
end_date = datetime.now().isoformat() + 'Z'
|
|
1310
|
+
|
|
1311
|
+
ct_analysis = call_lambda_api(
|
|
1312
|
+
'forensics',
|
|
1313
|
+
account_creds,
|
|
1314
|
+
[],
|
|
1315
|
+
operation='cloudtrail',
|
|
1316
|
+
account_id=acc_id,
|
|
1317
|
+
start_date=start_date,
|
|
1318
|
+
end_date=end_date,
|
|
1319
|
+
region='us-west-2'
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
cloudtrail_analyses.append(ct_analysis)
|
|
1323
|
+
|
|
1324
|
+
if ct_analysis.get('error'):
|
|
1325
|
+
click.echo(f" ⚠️ CloudTrail: {ct_analysis['error']}")
|
|
1326
|
+
else:
|
|
1327
|
+
click.echo(f" ✓ CloudTrail analysis complete")
|
|
1328
|
+
click.echo(f" - {len(ct_analysis['event_summary'])} event types")
|
|
1329
|
+
click.echo(f" - {len(ct_analysis['write_events'])} resource changes")
|
|
1330
|
+
except Exception as e:
|
|
1331
|
+
click.echo(f" ✗ CloudTrail error: {str(e)}")
|
|
1332
|
+
|
|
1333
|
+
# Generate report
|
|
1334
|
+
click.echo(f"\nGenerating report...")
|
|
1335
|
+
report = format_investigation_report(trends_data, inventories, cloudtrail_analyses if not no_cloudtrail else None)
|
|
1336
|
+
|
|
1337
|
+
# Write to file
|
|
1338
|
+
with open(output, 'w') as f:
|
|
1339
|
+
f.write(report)
|
|
1340
|
+
|
|
1341
|
+
click.echo(f"\n✓ Investigation complete!")
|
|
1342
|
+
click.echo(f"✓ Report saved to: {output}")
|
|
1343
|
+
click.echo("")
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def find_account_profile(account_id):
|
|
1347
|
+
"""
|
|
1348
|
+
Find the SSO profile name for a given account ID
|
|
1349
|
+
Returns profile name or None
|
|
1350
|
+
"""
|
|
1351
|
+
import subprocess
|
|
1352
|
+
|
|
1353
|
+
try:
|
|
1354
|
+
# Get list of profiles
|
|
1355
|
+
result = subprocess.run(
|
|
1356
|
+
['aws', 'configure', 'list-profiles'],
|
|
1357
|
+
capture_output=True,
|
|
1358
|
+
text=True
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
profiles = result.stdout.strip().split('\n')
|
|
1362
|
+
|
|
1363
|
+
# Check each profile
|
|
1364
|
+
for profile in profiles:
|
|
1365
|
+
try:
|
|
1366
|
+
result = subprocess.run(
|
|
1367
|
+
['aws', 'sts', 'get-caller-identity', '--profile', profile],
|
|
1368
|
+
capture_output=True,
|
|
1369
|
+
text=True,
|
|
1370
|
+
timeout=5
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
if account_id in result.stdout:
|
|
1374
|
+
return profile
|
|
1375
|
+
except:
|
|
1376
|
+
continue
|
|
1377
|
+
|
|
1378
|
+
return None
|
|
1379
|
+
except:
|
|
1380
|
+
return None
|
|
1381
|
+
|
|
1382
|
+
|
|
1087
1383
|
if __name__ == '__main__':
|
|
1088
1384
|
cli()
|