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,243 @@
|
|
1
|
+
"""
|
2
|
+
Lambda Function Inventory - Analyze and optimize Lambda function configurations.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import copy
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
import re
|
9
|
+
|
10
|
+
import click
|
11
|
+
from botocore.exceptions import ClientError
|
12
|
+
|
13
|
+
from .commons import (
|
14
|
+
display_aws_account_info,
|
15
|
+
get_client,
|
16
|
+
get_lambda_invocations,
|
17
|
+
get_lambda_total_duration,
|
18
|
+
write_to_csv,
|
19
|
+
)
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
# Lambda pricing (adjust for your region)
|
24
|
+
PRICE_PER_GB_SECOND = 0.00001667 # US East (N. Virginia) - update for your region
|
25
|
+
|
26
|
+
|
27
|
+
def update_iam_role_with_inline_policies(role_name, new_policy_document):
|
28
|
+
"""Update IAM role inline policies with improved error handling."""
|
29
|
+
try:
|
30
|
+
client_iam = get_client("iam")
|
31
|
+
|
32
|
+
for policy_name, policy in new_policy_document.items():
|
33
|
+
policy_string = json.dumps(policy)
|
34
|
+
|
35
|
+
try:
|
36
|
+
client_iam.put_role_policy(RoleName=role_name, PolicyName=policy_name, PolicyDocument=policy_string)
|
37
|
+
logger.info(f"✓ Updated policy '{policy_name}' for role '{role_name}'")
|
38
|
+
|
39
|
+
except ClientError as e:
|
40
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
41
|
+
logger.error(f"✗ Failed to update policy '{policy_name}' for role '{role_name}': {error_code}")
|
42
|
+
|
43
|
+
except Exception as e:
|
44
|
+
logger.error(f"Failed to update IAM role policies: {e}")
|
45
|
+
raise
|
46
|
+
|
47
|
+
|
48
|
+
def update_policy_document(policy_document):
|
49
|
+
new_policy_document = copy.deepcopy(policy_document)
|
50
|
+
changes = {}
|
51
|
+
|
52
|
+
for policy_name, policy in new_policy_document.items():
|
53
|
+
for statement in iterate_policy_statement(policy):
|
54
|
+
# Check if 'Action' is '*'
|
55
|
+
if statement["Action"] == "*" and statement["Resource"] != "*" and isinstance(statement["Resource"], str):
|
56
|
+
match = re.search(r"arn:aws:(\w+):", statement["Resource"])
|
57
|
+
if match and set(statement["Action"]) != {f"{match.group(1)}:*"}:
|
58
|
+
statement["Action"] = f"{match.group(1)}:*"
|
59
|
+
changes.setdefault(policy_name, []).append(statement["Action"])
|
60
|
+
elif isinstance(statement["Resource"], list) and statement["Action"] == "*":
|
61
|
+
statement["Action"] = []
|
62
|
+
for resource in statement["Resource"]:
|
63
|
+
match = re.search(r"arn:aws:(\w+):", resource)
|
64
|
+
if match and set(statement["Action"]) != {f"{match.group(1)}:*"}:
|
65
|
+
statement["Action"].append(f"{match.group(1)}:*")
|
66
|
+
changes.setdefault(policy_name, []).append(statement["Action"])
|
67
|
+
|
68
|
+
# Check if 'Resource' is '*'
|
69
|
+
if statement["Resource"] == "*" and statement["Action"] != "*" and isinstance(statement["Action"], str):
|
70
|
+
match = re.search(r"(\w+):", statement["Action"])
|
71
|
+
if match and set(statement["Resource"]) != {f"arn:aws:{match.group(1)}:*:*:*"}:
|
72
|
+
statement["Resource"] = f"arn:aws:{match.group(1)}:*:*:*"
|
73
|
+
changes.setdefault(policy_name, []).append(statement["Resource"])
|
74
|
+
elif isinstance(statement["Action"], list) and statement["Resource"] == "*":
|
75
|
+
statement["Resource"] = []
|
76
|
+
for action in statement["Action"]:
|
77
|
+
match = re.search(r"(\w+):", action)
|
78
|
+
if match and set(statement["Resource"]) != {f"arn:aws:{match.group(1)}:*:*:*"}:
|
79
|
+
statement["Resource"].append(f"arn:aws:{match.group(1)}:*:*:*")
|
80
|
+
changes.setdefault(policy_name, []).append(statement["Resource"])
|
81
|
+
|
82
|
+
return changes, new_policy_document
|
83
|
+
|
84
|
+
|
85
|
+
def iterate_policy_statement(policy):
|
86
|
+
if isinstance(policy["Statement"], list):
|
87
|
+
for statement in policy["Statement"]:
|
88
|
+
yield statement
|
89
|
+
elif isinstance(policy["Statement"], dict):
|
90
|
+
yield policy["Statement"]
|
91
|
+
|
92
|
+
|
93
|
+
def list_all_lambda_functions(client_lambda):
|
94
|
+
"""Generator that yields all Lambda functions with pagination."""
|
95
|
+
try:
|
96
|
+
paginator = client_lambda.get_paginator("list_functions")
|
97
|
+
|
98
|
+
for page in paginator.paginate():
|
99
|
+
for function in page["Functions"]:
|
100
|
+
yield function
|
101
|
+
|
102
|
+
except ClientError as e:
|
103
|
+
logger.error(f"Failed to list Lambda functions: {e}")
|
104
|
+
raise
|
105
|
+
|
106
|
+
|
107
|
+
@click.command()
|
108
|
+
@click.option(
|
109
|
+
"--dry-run", is_flag=True, default=True, help="Preview mode - show analysis without making policy changes"
|
110
|
+
)
|
111
|
+
@click.option("--output-file", default="lambda_functions.csv", help="Output CSV file path")
|
112
|
+
@click.option("--days", default=360, help="Number of days to analyze for invocations")
|
113
|
+
def list_lambda_functions(dry_run: bool = True, output_file: str = "lambda_functions.csv", days: int = 360):
|
114
|
+
"""Analyze Lambda functions, costs, and IAM policies with optimization suggestions."""
|
115
|
+
logger.info(f"Analyzing Lambda functions in {display_aws_account_info()}")
|
116
|
+
|
117
|
+
try:
|
118
|
+
# Initialize AWS clients
|
119
|
+
client_lambda = get_client("lambda")
|
120
|
+
client_cloudwatch = get_client("cloudwatch")
|
121
|
+
client_iam = get_client("iam")
|
122
|
+
|
123
|
+
# Get all Lambda functions
|
124
|
+
all_functions = list(list_all_lambda_functions(client_lambda))
|
125
|
+
|
126
|
+
if not all_functions:
|
127
|
+
logger.info("No Lambda functions found")
|
128
|
+
return
|
129
|
+
|
130
|
+
logger.info(f"Found {len(all_functions)} Lambda functions to analyze")
|
131
|
+
|
132
|
+
data = []
|
133
|
+
policy_updates_count = 0
|
134
|
+
|
135
|
+
# Analyze each function
|
136
|
+
for i, function in enumerate(all_functions, 1):
|
137
|
+
function_name = function["FunctionName"]
|
138
|
+
logger.info(f"Analyzing function {i}/{len(all_functions)}: {function_name}")
|
139
|
+
|
140
|
+
try:
|
141
|
+
# Get function metrics
|
142
|
+
invocations = get_lambda_invocations(function_name, days)
|
143
|
+
total_duration = get_lambda_total_duration(client_cloudwatch, function_name)
|
144
|
+
memory_size_gb = function["MemorySize"] / 1024 # Convert to GB
|
145
|
+
role_arn = function["Role"]
|
146
|
+
role_name = role_arn.split("/")[-1]
|
147
|
+
|
148
|
+
warnings = []
|
149
|
+
|
150
|
+
# Get IAM role policies
|
151
|
+
try:
|
152
|
+
attached_response = client_iam.list_attached_role_policies(RoleName=role_name)
|
153
|
+
attached_policies = attached_response.get("AttachedPolicies", [])
|
154
|
+
except ClientError as e:
|
155
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchEntity":
|
156
|
+
message = f"Role {role_name} does not exist for function {function_name}"
|
157
|
+
logger.warning(message)
|
158
|
+
warnings.append(message)
|
159
|
+
attached_policies = []
|
160
|
+
else:
|
161
|
+
raise
|
162
|
+
|
163
|
+
try:
|
164
|
+
inline_response = client_iam.list_role_policies(RoleName=role_name)
|
165
|
+
inline_policies = inline_response.get("PolicyNames", [])
|
166
|
+
except ClientError as e:
|
167
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchEntity":
|
168
|
+
inline_policies = []
|
169
|
+
else:
|
170
|
+
raise
|
171
|
+
|
172
|
+
# Get inline policy documents
|
173
|
+
inline_policy_documents = {}
|
174
|
+
for policy_name in inline_policies:
|
175
|
+
try:
|
176
|
+
policy_response = client_iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
|
177
|
+
inline_policy_documents[policy_name] = policy_response["PolicyDocument"]
|
178
|
+
except ClientError as e:
|
179
|
+
logger.warning(f"Could not get policy '{policy_name}' for role '{role_name}': {e}")
|
180
|
+
|
181
|
+
# Analyze and potentially update policies
|
182
|
+
changes, new_policy_document = update_policy_document(inline_policy_documents)
|
183
|
+
|
184
|
+
if changes:
|
185
|
+
logger.info(f" → Policy optimization recommendations found for {role_name}")
|
186
|
+
if not dry_run:
|
187
|
+
logger.info(f" → Updating policies for role: {role_name}")
|
188
|
+
update_iam_role_with_inline_policies(role_name, new_policy_document)
|
189
|
+
policy_updates_count += 1
|
190
|
+
else:
|
191
|
+
logger.info(f" → DRY-RUN: Would update policies for role: {role_name}")
|
192
|
+
|
193
|
+
# Calculate cost estimate
|
194
|
+
gb_seconds = (total_duration / 1000) * memory_size_gb
|
195
|
+
cost_estimate = gb_seconds * PRICE_PER_GB_SECOND
|
196
|
+
|
197
|
+
# Collect function data
|
198
|
+
function_data = {
|
199
|
+
"FunctionName": function_name,
|
200
|
+
"Runtime": function.get("Runtime", ""),
|
201
|
+
"MemorySize": function.get("MemorySize", 0),
|
202
|
+
"Timeout": function.get("Timeout", 0),
|
203
|
+
"LastModified": function.get("LastModified", ""),
|
204
|
+
"Description": function.get("Description", ""),
|
205
|
+
"Version": function.get("Version", ""),
|
206
|
+
f"Total Invocations in {days} days": invocations,
|
207
|
+
"Total Duration 30 days (seconds)": total_duration / 1000,
|
208
|
+
"Estimated Cost (30 days)": round(cost_estimate, 4),
|
209
|
+
"IAM Role": role_name,
|
210
|
+
"Attached Policies Count": len(attached_policies),
|
211
|
+
"Inline Policies Count": len(inline_policies),
|
212
|
+
"Policy Optimization": "Changes available" if changes else "No changes needed",
|
213
|
+
"Warnings": "; ".join(warnings) if warnings else "None",
|
214
|
+
}
|
215
|
+
|
216
|
+
data.append(function_data)
|
217
|
+
|
218
|
+
except Exception as e:
|
219
|
+
logger.error(f" ✗ Failed to analyze function {function_name}: {e}")
|
220
|
+
# Add minimal data for failed analysis
|
221
|
+
data.append({"FunctionName": function_name, "Error": str(e), "Status": "Analysis Failed"})
|
222
|
+
|
223
|
+
# Export results
|
224
|
+
write_to_csv(data, output_file)
|
225
|
+
logger.info(f"Lambda analysis exported to: {output_file}")
|
226
|
+
|
227
|
+
# Summary
|
228
|
+
logger.info("\n=== ANALYSIS SUMMARY ===")
|
229
|
+
logger.info(f"Functions analyzed: {len(all_functions)}")
|
230
|
+
logger.info(
|
231
|
+
f"Functions with policy optimizations: {sum(1 for d in data if d.get('Policy Optimization') == 'Changes available')}"
|
232
|
+
)
|
233
|
+
|
234
|
+
if dry_run and policy_updates_count == 0:
|
235
|
+
policy_candidates = sum(1 for d in data if d.get("Policy Optimization") == "Changes available")
|
236
|
+
if policy_candidates > 0:
|
237
|
+
logger.info(f"To apply {policy_candidates} policy optimizations, run with --no-dry-run")
|
238
|
+
elif not dry_run:
|
239
|
+
logger.info(f"Applied policy updates to {policy_updates_count} roles")
|
240
|
+
|
241
|
+
except Exception as e:
|
242
|
+
logger.error(f"Failed to analyze Lambda functions: {e}")
|
243
|
+
raise
|