runbooks 0.7.0__py3-none-any.whl → 0.7.6__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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
"""
|
2
|
+
🚨 HIGH-RISK: Cognito Password Reset - Handle user authentication with extreme care.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
import click
|
8
|
+
from botocore.exceptions import ClientError
|
9
|
+
|
10
|
+
from .commons import display_aws_account_info, get_client
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
@click.command()
|
16
|
+
@click.option("--user-pool-id", help="Cognito User Pool ID (interactive mode if not provided)")
|
17
|
+
@click.option("--username", help="Username to reset (interactive mode if not provided)")
|
18
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts (dangerous!)")
|
19
|
+
def reset_password(user_pool_id, username, confirm):
|
20
|
+
"""🚨 HIGH-RISK: Reset user password in Cognito User Pool with safety checks."""
|
21
|
+
|
22
|
+
# HIGH-RISK OPERATION WARNING
|
23
|
+
if not confirm:
|
24
|
+
logger.warning("🚨 HIGH-RISK OPERATION: User password reset")
|
25
|
+
logger.warning("This operation will reset user authentication credentials")
|
26
|
+
if not click.confirm("Do you want to continue?"):
|
27
|
+
logger.info("Operation cancelled by user")
|
28
|
+
return
|
29
|
+
|
30
|
+
logger.info(f"🔐 Cognito password reset in {display_aws_account_info()}")
|
31
|
+
|
32
|
+
try:
|
33
|
+
client = get_client("cognito-idp")
|
34
|
+
|
35
|
+
# Get User Pool ID if not provided
|
36
|
+
if not user_pool_id:
|
37
|
+
logger.info("Listing available User Pools...")
|
38
|
+
response = client.list_user_pools(MaxResults=60)
|
39
|
+
user_pools = response.get("UserPools", [])
|
40
|
+
|
41
|
+
if not user_pools:
|
42
|
+
logger.error("No User Pools found")
|
43
|
+
return
|
44
|
+
|
45
|
+
logger.info("\n📋 Available User Pools:")
|
46
|
+
for i, pool in enumerate(user_pools, 1):
|
47
|
+
logger.info(f" {i}. {pool['Name']} (ID: {pool['Id']})")
|
48
|
+
|
49
|
+
user_pool_id = click.prompt("\nPlease enter the User Pool ID", type=str)
|
50
|
+
|
51
|
+
# Validate User Pool exists
|
52
|
+
try:
|
53
|
+
client.describe_user_pool(UserPoolId=user_pool_id)
|
54
|
+
logger.info(f"✓ User Pool {user_pool_id} found")
|
55
|
+
except ClientError as e:
|
56
|
+
if e.response.get("Error", {}).get("Code") == "ResourceNotFoundException":
|
57
|
+
logger.error(f"❌ User Pool not found: {user_pool_id}")
|
58
|
+
return
|
59
|
+
raise
|
60
|
+
|
61
|
+
# Get username if not provided
|
62
|
+
if not username:
|
63
|
+
logger.info("Listing users in the pool...")
|
64
|
+
try:
|
65
|
+
response = client.list_users(UserPoolId=user_pool_id, Limit=50)
|
66
|
+
users = response.get("Users", [])
|
67
|
+
|
68
|
+
if not users:
|
69
|
+
logger.error("No users found in the User Pool")
|
70
|
+
return
|
71
|
+
|
72
|
+
logger.info("\n👥 Available Users:")
|
73
|
+
for i, user in enumerate(users, 1):
|
74
|
+
status = user.get("UserStatus", "Unknown")
|
75
|
+
enabled = user.get("Enabled", False)
|
76
|
+
logger.info(f" {i}. {user['Username']} (Status: {status}, Enabled: {enabled})")
|
77
|
+
|
78
|
+
username = click.prompt("\nPlease enter the username", type=str)
|
79
|
+
|
80
|
+
except ClientError as e:
|
81
|
+
logger.error(f"Failed to list users: {e}")
|
82
|
+
return
|
83
|
+
|
84
|
+
# Validate user exists and get details
|
85
|
+
try:
|
86
|
+
user_info = client.admin_get_user(UserPoolId=user_pool_id, Username=username)
|
87
|
+
user_status = user_info.get("UserStatus", "Unknown")
|
88
|
+
user_enabled = user_info.get("Enabled", False)
|
89
|
+
|
90
|
+
logger.info(f"\n📝 User Details:")
|
91
|
+
logger.info(f" Username: {username}")
|
92
|
+
logger.info(f" Status: {user_status}")
|
93
|
+
logger.info(f" Enabled: {user_enabled}")
|
94
|
+
|
95
|
+
except ClientError as e:
|
96
|
+
if e.response.get("Error", {}).get("Code") == "UserNotFoundException":
|
97
|
+
logger.error(f"❌ User not found: {username}")
|
98
|
+
return
|
99
|
+
raise
|
100
|
+
|
101
|
+
# Get new password and settings
|
102
|
+
password = click.prompt("Please enter the new password", type=str, hide_input=True, confirmation_prompt=True)
|
103
|
+
permanent = click.prompt("Set the password as permanent? (default: temporary)", type=bool, default=False)
|
104
|
+
|
105
|
+
# FINAL CONFIRMATION for high-risk operation
|
106
|
+
if not confirm:
|
107
|
+
logger.warning(f"\n🚨 FINAL CONFIRMATION:")
|
108
|
+
logger.warning(f" User Pool: {user_pool_id}")
|
109
|
+
logger.warning(f" Username: {username}")
|
110
|
+
logger.warning(f" Permanent: {permanent}")
|
111
|
+
if not click.confirm("Proceed with password reset?"):
|
112
|
+
logger.info("Operation cancelled")
|
113
|
+
return
|
114
|
+
|
115
|
+
# Perform password reset
|
116
|
+
logger.info(f"🔄 Resetting password for user: {username}")
|
117
|
+
try:
|
118
|
+
response = client.admin_set_user_password(
|
119
|
+
UserPoolId=user_pool_id, Username=username, Password=password, Permanent=permanent
|
120
|
+
)
|
121
|
+
logger.info(f"✅ Password reset successful for user: {username}")
|
122
|
+
|
123
|
+
# Log the operation for audit trail
|
124
|
+
logger.info(f"🔍 Audit: Password reset completed")
|
125
|
+
logger.info(f" User Pool: {user_pool_id}")
|
126
|
+
logger.info(f" Username: {username}")
|
127
|
+
logger.info(f" Permanent: {permanent}")
|
128
|
+
|
129
|
+
except ClientError as e:
|
130
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
131
|
+
if error_code == "UserNotFoundException":
|
132
|
+
logger.error(f"❌ User not found: {username}")
|
133
|
+
elif error_code == "InvalidPasswordException":
|
134
|
+
logger.error("❌ Invalid password - check User Pool password policy")
|
135
|
+
elif error_code == "NotAuthorizedException":
|
136
|
+
logger.error("❌ Not authorized - check IAM permissions")
|
137
|
+
else:
|
138
|
+
logger.error(f"❌ Failed to reset password: {e}")
|
139
|
+
return
|
140
|
+
|
141
|
+
# Optional: Add user to ReadHistorical group (if needed)
|
142
|
+
group_name = "ReadHistorical"
|
143
|
+
if click.confirm(f"Add user to group '{group_name}'?", default=False):
|
144
|
+
try:
|
145
|
+
# Check if user is already in the group
|
146
|
+
response = client.admin_list_groups_for_user(UserPoolId=user_pool_id, Username=username)
|
147
|
+
existing_groups = [group["GroupName"] for group in response.get("Groups", [])]
|
148
|
+
|
149
|
+
if group_name in existing_groups:
|
150
|
+
logger.info(f"ℹ User already in group: {group_name}")
|
151
|
+
else:
|
152
|
+
client.admin_add_user_to_group(UserPoolId=user_pool_id, Username=username, GroupName=group_name)
|
153
|
+
logger.info(f"✅ User added to group: {group_name}")
|
154
|
+
|
155
|
+
except ClientError as e:
|
156
|
+
logger.error(f"❌ Failed to add user to group: {e}")
|
157
|
+
|
158
|
+
except ClientError as e:
|
159
|
+
logger.error(f"❌ Cognito operation failed: {e}")
|
160
|
+
raise
|
161
|
+
except Exception as e:
|
162
|
+
logger.error(f"❌ Unexpected error: {e}")
|
163
|
+
raise
|
@@ -0,0 +1,455 @@
|
|
1
|
+
import configparser
|
2
|
+
import csv
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import time
|
7
|
+
import webbrowser
|
8
|
+
from datetime import datetime, timedelta
|
9
|
+
from functools import lru_cache as LRU_cache
|
10
|
+
|
11
|
+
import boto3
|
12
|
+
import botocore.exceptions
|
13
|
+
import botocore.session
|
14
|
+
from botocore.exceptions import ClientError
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
def get_all_available_aws_credentials(start_url: str = None, role_name="power-user") -> dict:
|
20
|
+
if not start_url:
|
21
|
+
raise ValueError("Start URL for AWS SSO is required")
|
22
|
+
|
23
|
+
credentials = {}
|
24
|
+
|
25
|
+
# Create an SSO OIDC client
|
26
|
+
sso_oidc = boto3.client("sso-oidc", region_name="ap-southeast-2")
|
27
|
+
|
28
|
+
try:
|
29
|
+
# Register client
|
30
|
+
client_creds = sso_oidc.register_client(
|
31
|
+
clientName="MyApp",
|
32
|
+
clientType="public",
|
33
|
+
)
|
34
|
+
|
35
|
+
# Get device authorization
|
36
|
+
device_auth = sso_oidc.start_device_authorization(
|
37
|
+
clientId=client_creds["clientId"], clientSecret=client_creds["clientSecret"], startUrl=start_url
|
38
|
+
)
|
39
|
+
|
40
|
+
print(f"Please go to {device_auth['verificationUriComplete']} and enter the code: {device_auth['userCode']}")
|
41
|
+
webbrowser.open(device_auth["verificationUriComplete"])
|
42
|
+
|
43
|
+
# Wait for user to authorize
|
44
|
+
token = None
|
45
|
+
max_retries = 60 # Maximum number of retries (5 minutes with 5-second intervals)
|
46
|
+
retry_count = 0
|
47
|
+
|
48
|
+
while not token and retry_count < max_retries:
|
49
|
+
try:
|
50
|
+
token = sso_oidc.create_token(
|
51
|
+
clientId=client_creds["clientId"],
|
52
|
+
clientSecret=client_creds["clientSecret"],
|
53
|
+
grantType="urn:ietf:params:oauth:grant-type:device_code",
|
54
|
+
deviceCode=device_auth["deviceCode"],
|
55
|
+
)
|
56
|
+
except sso_oidc.exceptions.AuthorizationPendingException:
|
57
|
+
print("Waiting for authorization... Please complete the process in your browser.")
|
58
|
+
time.sleep(5) # Wait for 5 seconds before trying again
|
59
|
+
retry_count += 1
|
60
|
+
except Exception as e:
|
61
|
+
print(f"An error occurred: {e}")
|
62
|
+
break
|
63
|
+
|
64
|
+
if not token:
|
65
|
+
print("Authorization timed out or failed. Please try again.")
|
66
|
+
return credentials
|
67
|
+
|
68
|
+
# Create SSO client
|
69
|
+
sso = boto3.client("sso", region_name="ap-southeast-2")
|
70
|
+
|
71
|
+
# List accounts (with pagination)
|
72
|
+
all_accounts = []
|
73
|
+
paginator = sso.get_paginator("list_accounts")
|
74
|
+
for page in paginator.paginate(accessToken=token["accessToken"]):
|
75
|
+
all_accounts.extend(page["accountList"])
|
76
|
+
|
77
|
+
for account in all_accounts:
|
78
|
+
# Get role names for each account
|
79
|
+
roles = []
|
80
|
+
role_paginator = sso.get_paginator("list_account_roles")
|
81
|
+
for role_page in role_paginator.paginate(accessToken=token["accessToken"], accountId=account["accountId"]):
|
82
|
+
roles.extend(role_page["roleList"])
|
83
|
+
|
84
|
+
for role in roles:
|
85
|
+
# Get temporary credentials for each role
|
86
|
+
if role["roleName"] == role_name:
|
87
|
+
creds = sso.get_role_credentials(
|
88
|
+
accessToken=token["accessToken"], accountId=account["accountId"], roleName=role["roleName"]
|
89
|
+
)
|
90
|
+
|
91
|
+
credentials[f"{account['accountId']}_{role['roleName']}"] = {
|
92
|
+
"aws_access_key_id": creds["roleCredentials"]["accessKeyId"],
|
93
|
+
"aws_secret_access_key": creds["roleCredentials"]["secretAccessKey"],
|
94
|
+
"aws_session_token": creds["roleCredentials"]["sessionToken"],
|
95
|
+
}
|
96
|
+
|
97
|
+
except ClientError as e:
|
98
|
+
logger.error(f"An error occurred: {e}")
|
99
|
+
|
100
|
+
return credentials
|
101
|
+
|
102
|
+
|
103
|
+
def read_all_aws_credentials(file_path: str = "credentials") -> dict:
|
104
|
+
config = configparser.ConfigParser()
|
105
|
+
config.read(file_path) # replace with your file path if not in the same directory
|
106
|
+
|
107
|
+
credentials = {}
|
108
|
+
for profile_name in config.sections():
|
109
|
+
aws_access_key_id = config.get(profile_name, "aws_access_key_id")
|
110
|
+
aws_secret_access_key = config.get(profile_name, "aws_secret_access_key")
|
111
|
+
aws_session_token = config.get(profile_name, "aws_session_token")
|
112
|
+
|
113
|
+
credentials[profile_name] = {
|
114
|
+
"aws_access_key_id": aws_access_key_id,
|
115
|
+
"aws_secret_access_key": aws_secret_access_key,
|
116
|
+
"aws_session_token": aws_session_token,
|
117
|
+
}
|
118
|
+
|
119
|
+
return credentials
|
120
|
+
|
121
|
+
|
122
|
+
def get_api_gateways(client):
|
123
|
+
response = client.get_rest_apis()
|
124
|
+
return response["items"]
|
125
|
+
|
126
|
+
|
127
|
+
def get_stages(client, rest_api_id):
|
128
|
+
response = client.get_stages(restApiId=rest_api_id)
|
129
|
+
return response["item"]
|
130
|
+
|
131
|
+
|
132
|
+
@LRU_cache(maxsize=32)
|
133
|
+
def get_resources(client, rest_api_id):
|
134
|
+
response = client.get_resources(restApiId=rest_api_id)
|
135
|
+
return response["items"]
|
136
|
+
|
137
|
+
|
138
|
+
def get_method_details(client, rest_api_id, resource_id, http_method):
|
139
|
+
try:
|
140
|
+
response = client.get_method(restApiId=rest_api_id, resourceId=resource_id, httpMethod=http_method)
|
141
|
+
return response
|
142
|
+
except botocore.exceptions.ClientError as e:
|
143
|
+
if e.response["Error"]["Code"] == "NotFoundException":
|
144
|
+
# logging.info(f"Method {http_method} not found for resource {resource_id} in API {rest_api_id}")
|
145
|
+
return None
|
146
|
+
else:
|
147
|
+
raise
|
148
|
+
|
149
|
+
|
150
|
+
def get_client(client_name: str):
|
151
|
+
return boto3.client(
|
152
|
+
client_name,
|
153
|
+
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
|
154
|
+
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
155
|
+
aws_session_token=os.environ.get("AWS_SESSION_TOKEN"),
|
156
|
+
region_name=os.environ.get("AWS_REGION"), # or your preferred region
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
def get_resource(client_name: str):
|
161
|
+
return boto3.resource(
|
162
|
+
client_name,
|
163
|
+
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
|
164
|
+
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
165
|
+
aws_session_token=os.environ.get("AWS_SESSION_TOKEN"),
|
166
|
+
region_name=os.environ.get("AWS_REGION"), # or your preferred region
|
167
|
+
)
|
168
|
+
|
169
|
+
|
170
|
+
def get_log_groups(client, log_group_name_prefix):
|
171
|
+
response = client.describe_log_groups(logGroupNamePrefix=log_group_name_prefix)
|
172
|
+
return response["logGroups"]
|
173
|
+
|
174
|
+
|
175
|
+
def write_to_csv(data, filename):
|
176
|
+
"""Write data to a CSV file.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
data (list of dict): The data to write. Each dict is a row in the CSV.
|
180
|
+
filename (str): The name of the CSV file.
|
181
|
+
"""
|
182
|
+
with open(filename, "w", newline="") as csvfile:
|
183
|
+
fieldnames = data[0].keys()
|
184
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
185
|
+
|
186
|
+
writer.writeheader()
|
187
|
+
for row in data:
|
188
|
+
writer.writerow(row)
|
189
|
+
|
190
|
+
|
191
|
+
@LRU_cache(maxsize=32)
|
192
|
+
def get_log_group_from_lambda(client_lambda, function_name):
|
193
|
+
try:
|
194
|
+
response = client_lambda.get_function_configuration(FunctionName=function_name)
|
195
|
+
|
196
|
+
if "LoggingConfig" in response:
|
197
|
+
return response["LoggingConfig"]["LogGroup"]
|
198
|
+
|
199
|
+
else:
|
200
|
+
return None # Lambda logging might not be configured
|
201
|
+
|
202
|
+
except client_lambda.exceptions.ResourceNotFoundException:
|
203
|
+
message = f"Lambda function '{function_name}' not found."
|
204
|
+
logger.info(message)
|
205
|
+
return message
|
206
|
+
|
207
|
+
|
208
|
+
@LRU_cache(maxsize=32)
|
209
|
+
def get_lambda_config(client_lambda, function_name):
|
210
|
+
try:
|
211
|
+
response = client_lambda.get_function_configuration(FunctionName=function_name)
|
212
|
+
|
213
|
+
return response
|
214
|
+
|
215
|
+
except client_lambda.exceptions.ResourceNotFoundException:
|
216
|
+
message = f"Lambda function '{function_name}' not found."
|
217
|
+
logger.info(message)
|
218
|
+
return message
|
219
|
+
|
220
|
+
|
221
|
+
@LRU_cache(maxsize=32)
|
222
|
+
def get_lambda_invocations(function_name, days=30):
|
223
|
+
client_cloudwatch = get_client("cloudwatch")
|
224
|
+
# Define the time period
|
225
|
+
end_time = datetime.now()
|
226
|
+
start_time = end_time - timedelta(days=days)
|
227
|
+
|
228
|
+
# Get the metric statistics
|
229
|
+
response = client_cloudwatch.get_metric_statistics(
|
230
|
+
Namespace="AWS/Lambda",
|
231
|
+
MetricName="Invocations",
|
232
|
+
Dimensions=[
|
233
|
+
{"Name": "FunctionName", "Value": function_name},
|
234
|
+
],
|
235
|
+
StartTime=start_time,
|
236
|
+
EndTime=end_time,
|
237
|
+
Period=3600 * 24, # One day periods
|
238
|
+
Statistics=[
|
239
|
+
"Sum", # Get the total (sum) of the invocations
|
240
|
+
],
|
241
|
+
)
|
242
|
+
|
243
|
+
# Calculate the total invocations over the time period
|
244
|
+
total_invocations = sum(datapoint["Sum"] for datapoint in response["Datapoints"])
|
245
|
+
|
246
|
+
return total_invocations
|
247
|
+
|
248
|
+
|
249
|
+
@LRU_cache(maxsize=32)
|
250
|
+
def get_api_gateway_calls(api_name, days=30):
|
251
|
+
# Create a CloudWatch client
|
252
|
+
client = get_client("cloudwatch")
|
253
|
+
|
254
|
+
# Define the time period
|
255
|
+
end_time = datetime.now()
|
256
|
+
start_time = end_time - timedelta(days=days)
|
257
|
+
|
258
|
+
# Get the metric statistics
|
259
|
+
response = client.get_metric_statistics(
|
260
|
+
Namespace="AWS/ApiGateway",
|
261
|
+
MetricName="Count",
|
262
|
+
Dimensions=[
|
263
|
+
{"Name": "ApiName", "Value": api_name},
|
264
|
+
],
|
265
|
+
StartTime=start_time,
|
266
|
+
EndTime=end_time,
|
267
|
+
Period=3600 * 24, # One day periods
|
268
|
+
Statistics=[
|
269
|
+
"Sum", # Get the total (sum) of the API calls
|
270
|
+
],
|
271
|
+
)
|
272
|
+
|
273
|
+
# Calculate the total API calls over the time period
|
274
|
+
total_calls = sum(datapoint["Sum"] for datapoint in response["Datapoints"])
|
275
|
+
|
276
|
+
return total_calls
|
277
|
+
|
278
|
+
|
279
|
+
def get_lambda_total_duration(client_cloudwatch, function_name, days=30):
|
280
|
+
# Define the time period
|
281
|
+
end_time = datetime.now()
|
282
|
+
start_time = end_time - timedelta(days=days)
|
283
|
+
|
284
|
+
# Get the metric statistics for Duration
|
285
|
+
response_duration = client_cloudwatch.get_metric_statistics(
|
286
|
+
Namespace="AWS/Lambda",
|
287
|
+
MetricName="Duration",
|
288
|
+
Dimensions=[
|
289
|
+
{"Name": "FunctionName", "Value": function_name},
|
290
|
+
],
|
291
|
+
StartTime=start_time,
|
292
|
+
EndTime=end_time,
|
293
|
+
Period=3600, # One hour periods
|
294
|
+
Statistics=[
|
295
|
+
"Sum", # Get the total duration
|
296
|
+
],
|
297
|
+
)
|
298
|
+
|
299
|
+
# Calculate the total duration over the time period
|
300
|
+
total_duration = (
|
301
|
+
sum(datapoint["Sum"] for datapoint in response_duration["Datapoints"]) if response_duration["Datapoints"] else 0
|
302
|
+
)
|
303
|
+
|
304
|
+
return total_duration
|
305
|
+
|
306
|
+
|
307
|
+
def get_price(service_code, region_name, instance_type):
|
308
|
+
"""Get the on-demand price for an instance type in a region"""
|
309
|
+
response = get_product_pricing(instance_type, region_name, service_code)
|
310
|
+
|
311
|
+
for product in response["PriceList"]:
|
312
|
+
product_obj = json.loads(product)
|
313
|
+
product_terms = product_obj["terms"]
|
314
|
+
|
315
|
+
for term in product_terms["OnDemand"]:
|
316
|
+
price_dimensions = product_terms["OnDemand"][term]["priceDimensions"]
|
317
|
+
|
318
|
+
for dimension in price_dimensions:
|
319
|
+
price_info = price_dimensions[dimension]
|
320
|
+
price = float(price_info["pricePerUnit"]["USD"])
|
321
|
+
currency = list(price_info["pricePerUnit"].keys())[0] # Get the currency
|
322
|
+
if not price:
|
323
|
+
continue
|
324
|
+
return price, currency
|
325
|
+
|
326
|
+
return None
|
327
|
+
|
328
|
+
|
329
|
+
@LRU_cache(maxsize=32)
|
330
|
+
def get_product_pricing(instance_type, region_name, service_code):
|
331
|
+
# Pricing API available only in selected regions
|
332
|
+
pricing = botocore.session.get_session().create_client("pricing", region_name="us-east-1")
|
333
|
+
response = pricing.get_products(
|
334
|
+
ServiceCode=service_code,
|
335
|
+
Filters=[
|
336
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": region_name},
|
337
|
+
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_type},
|
338
|
+
{"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
|
339
|
+
{"Type": "TERM_MATCH", "Field": "termType", "Value": "OnDemand"},
|
340
|
+
],
|
341
|
+
MaxResults=100,
|
342
|
+
)
|
343
|
+
return response
|
344
|
+
|
345
|
+
|
346
|
+
def get_role_permissions(role_name):
|
347
|
+
# Create a client for IAM
|
348
|
+
iam_client = get_client("iam")
|
349
|
+
|
350
|
+
# Get the policies attached to the role
|
351
|
+
response = iam_client.list_attached_role_policies(RoleName=role_name)
|
352
|
+
|
353
|
+
# The policies are in the 'AttachedPolicies' field of the response
|
354
|
+
attached_policies = response["AttachedPolicies"]
|
355
|
+
|
356
|
+
response = iam_client.list_role_policies(RoleName=role_name)
|
357
|
+
|
358
|
+
inline_policies = response["PolicyNames"]
|
359
|
+
|
360
|
+
inline_policy_documents = {}
|
361
|
+
for policy_name in inline_policies:
|
362
|
+
policy_document = iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)["PolicyDocument"]
|
363
|
+
inline_policy_documents[policy_name] = policy_document
|
364
|
+
|
365
|
+
return attached_policies, inline_policy_documents
|
366
|
+
|
367
|
+
|
368
|
+
def get_vpc_flow_logs():
|
369
|
+
# Create a client for EC2
|
370
|
+
ec2_client = get_client("ec2")
|
371
|
+
|
372
|
+
# Get a list of flow logs
|
373
|
+
response = ec2_client.describe_flow_logs()
|
374
|
+
|
375
|
+
# The flow logs are in the 'FlowLogs' field of the response
|
376
|
+
flow_logs = response["FlowLogs"]
|
377
|
+
|
378
|
+
# Create a dictionary to store the flow logs for each VPC
|
379
|
+
vpc_flow_logs = {}
|
380
|
+
|
381
|
+
# Iterate over the flow logs
|
382
|
+
for flow_log in flow_logs:
|
383
|
+
# Get the ID of the VPC that the flow log is attached to
|
384
|
+
vpc_id = flow_log["ResourceId"]
|
385
|
+
|
386
|
+
# If the VPC ID is not already in the dictionary, add it
|
387
|
+
if vpc_id not in vpc_flow_logs:
|
388
|
+
vpc_flow_logs[vpc_id] = []
|
389
|
+
|
390
|
+
# Add the flow log to the list for this VPC
|
391
|
+
vpc_flow_logs[vpc_id].append(flow_log)
|
392
|
+
|
393
|
+
return vpc_flow_logs
|
394
|
+
|
395
|
+
|
396
|
+
def get_ec2_instances_by_security_groups(security_group_ids):
|
397
|
+
ec2 = get_resource("ec2") # Use resource instead of client
|
398
|
+
|
399
|
+
# Filter instances based on security groups
|
400
|
+
instances = ec2.instances.filter(Filters=[{"Name": "instance.group-id", "Values": security_group_ids}])
|
401
|
+
|
402
|
+
instance_info = []
|
403
|
+
for instance in instances:
|
404
|
+
instance_info.append({"InstanceId": instance.id, "State": instance.state["Name"], "Tags": instance.tags})
|
405
|
+
|
406
|
+
return instance_info
|
407
|
+
|
408
|
+
|
409
|
+
@LRU_cache(maxsize=32)
|
410
|
+
def get_bucket_policy(bucket_name) -> tuple:
|
411
|
+
s3 = get_client("s3")
|
412
|
+
try:
|
413
|
+
result = s3.get_bucket_policy(Bucket=bucket_name)
|
414
|
+
policy = json.loads(result["Policy"])
|
415
|
+
except ClientError as e:
|
416
|
+
if e.response["Error"]["Code"] == "NoSuchBucketPolicy":
|
417
|
+
policy = "No policy"
|
418
|
+
else:
|
419
|
+
policy = e.response["Error"]["Message"]
|
420
|
+
|
421
|
+
try:
|
422
|
+
public_access_block = s3.get_public_access_block(Bucket=bucket_name)
|
423
|
+
except ClientError as e:
|
424
|
+
if e.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration":
|
425
|
+
public_access_block = "No public access block configuration"
|
426
|
+
else:
|
427
|
+
public_access_block = e.response["Error"]["Message"]
|
428
|
+
else:
|
429
|
+
if isinstance(public_access_block, dict):
|
430
|
+
public_access_block = public_access_block.get("PublicAccessBlockConfiguration", {})
|
431
|
+
|
432
|
+
return policy, public_access_block
|
433
|
+
|
434
|
+
|
435
|
+
def display_aws_account_info():
|
436
|
+
sts = get_client("sts")
|
437
|
+
|
438
|
+
# Get caller identity
|
439
|
+
identity = sts.get_caller_identity()
|
440
|
+
|
441
|
+
# Get account id
|
442
|
+
account_id = identity["Account"]
|
443
|
+
|
444
|
+
# Get IAM user or role name
|
445
|
+
user_or_role_name = identity["Arn"].split("/")[-2]
|
446
|
+
|
447
|
+
return {"AWS Account ID": account_id, "AWS User or Role Name": user_or_role_name}
|
448
|
+
|
449
|
+
|
450
|
+
def list_tables():
|
451
|
+
"""Lists all DynamoDB tables in the account."""
|
452
|
+
dynamodb = get_client("dynamodb")
|
453
|
+
paginator = dynamodb.get_paginator("list_tables")
|
454
|
+
for page in paginator.paginate():
|
455
|
+
yield from page["TableNames"]
|