runbooks 0.7.0__py3-none-any.whl → 0.7.5__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.
Files changed (100) hide show
  1. runbooks/__init__.py +87 -37
  2. runbooks/cfat/README.md +300 -49
  3. runbooks/cfat/__init__.py +2 -2
  4. runbooks/finops/__init__.py +1 -1
  5. runbooks/finops/cli.py +1 -1
  6. runbooks/inventory/collectors/__init__.py +8 -0
  7. runbooks/inventory/collectors/aws_management.py +791 -0
  8. runbooks/inventory/collectors/aws_networking.py +3 -3
  9. runbooks/main.py +3389 -782
  10. runbooks/operate/__init__.py +207 -0
  11. runbooks/operate/base.py +311 -0
  12. runbooks/operate/cloudformation_operations.py +619 -0
  13. runbooks/operate/cloudwatch_operations.py +496 -0
  14. runbooks/operate/dynamodb_operations.py +812 -0
  15. runbooks/operate/ec2_operations.py +926 -0
  16. runbooks/operate/iam_operations.py +569 -0
  17. runbooks/operate/s3_operations.py +1211 -0
  18. runbooks/operate/tagging_operations.py +655 -0
  19. runbooks/remediation/CLAUDE.md +100 -0
  20. runbooks/remediation/DOME9.md +218 -0
  21. runbooks/remediation/README.md +26 -0
  22. runbooks/remediation/Tests/__init__.py +0 -0
  23. runbooks/remediation/Tests/update_policy.py +74 -0
  24. runbooks/remediation/__init__.py +95 -0
  25. runbooks/remediation/acm_cert_expired_unused.py +98 -0
  26. runbooks/remediation/acm_remediation.py +875 -0
  27. runbooks/remediation/api_gateway_list.py +167 -0
  28. runbooks/remediation/base.py +643 -0
  29. runbooks/remediation/cloudtrail_remediation.py +908 -0
  30. runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
  31. runbooks/remediation/cognito_active_users.py +78 -0
  32. runbooks/remediation/cognito_remediation.py +856 -0
  33. runbooks/remediation/cognito_user_password_reset.py +163 -0
  34. runbooks/remediation/commons.py +455 -0
  35. runbooks/remediation/dynamodb_optimize.py +155 -0
  36. runbooks/remediation/dynamodb_remediation.py +744 -0
  37. runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
  38. runbooks/remediation/ec2_public_ips.py +134 -0
  39. runbooks/remediation/ec2_remediation.py +892 -0
  40. runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
  41. runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
  42. runbooks/remediation/ec2_unused_security_groups.py +202 -0
  43. runbooks/remediation/kms_enable_key_rotation.py +651 -0
  44. runbooks/remediation/kms_remediation.py +717 -0
  45. runbooks/remediation/lambda_list.py +243 -0
  46. runbooks/remediation/lambda_remediation.py +971 -0
  47. runbooks/remediation/multi_account.py +569 -0
  48. runbooks/remediation/rds_instance_list.py +199 -0
  49. runbooks/remediation/rds_remediation.py +873 -0
  50. runbooks/remediation/rds_snapshot_list.py +192 -0
  51. runbooks/remediation/requirements.txt +118 -0
  52. runbooks/remediation/s3_block_public_access.py +159 -0
  53. runbooks/remediation/s3_bucket_public_access.py +143 -0
  54. runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
  55. runbooks/remediation/s3_downloader.py +215 -0
  56. runbooks/remediation/s3_enable_access_logging.py +562 -0
  57. runbooks/remediation/s3_encryption.py +526 -0
  58. runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
  59. runbooks/remediation/s3_list.py +141 -0
  60. runbooks/remediation/s3_object_search.py +201 -0
  61. runbooks/remediation/s3_remediation.py +816 -0
  62. runbooks/remediation/scan_for_phrase.py +425 -0
  63. runbooks/remediation/workspaces_list.py +220 -0
  64. runbooks/security/__init__.py +9 -10
  65. runbooks/security/security_baseline_tester.py +4 -2
  66. runbooks-0.7.5.dist-info/METADATA +606 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/RECORD +72 -44
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/entry_points.txt +0 -1
  69. runbooks/aws/__init__.py +0 -58
  70. runbooks/aws/dynamodb_operations.py +0 -231
  71. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  72. runbooks/aws/ec2_describe_instances.py +0 -202
  73. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  74. runbooks/aws/ec2_run_instances.py +0 -213
  75. runbooks/aws/ec2_start_stop_instances.py +0 -212
  76. runbooks/aws/ec2_terminate_instances.py +0 -143
  77. runbooks/aws/ec2_unused_eips.py +0 -196
  78. runbooks/aws/ec2_unused_volumes.py +0 -188
  79. runbooks/aws/s3_create_bucket.py +0 -142
  80. runbooks/aws/s3_list_buckets.py +0 -152
  81. runbooks/aws/s3_list_objects.py +0 -156
  82. runbooks/aws/s3_object_operations.py +0 -183
  83. runbooks/aws/tagging_lambda_handler.py +0 -183
  84. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  85. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  86. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  87. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  88. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  89. runbooks/inventory/update_aws_actions.py +0 -173
  90. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  91. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  92. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  93. runbooks/inventory/update_s3_public_access_block.py +0 -539
  94. runbooks/organizations/__init__.py +0 -12
  95. runbooks/organizations/manager.py +0 -374
  96. runbooks-0.7.0.dist-info/METADATA +0 -375
  97. /runbooks/{aws → operate}/tags.json +0 -0
  98. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/WHEEL +0 -0
  99. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/licenses/LICENSE +0 -0
  100. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/top_level.txt +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