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.
Files changed (132) 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.6.dist-info/METADATA +608 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
  69. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
  70. jupyter-agent/.env +0 -2
  71. jupyter-agent/.env.template +0 -2
  72. jupyter-agent/.gitattributes +0 -35
  73. jupyter-agent/.gradio/certificate.pem +0 -31
  74. jupyter-agent/README.md +0 -16
  75. jupyter-agent/__main__.log +0 -8
  76. jupyter-agent/app.py +0 -256
  77. jupyter-agent/cloudops-agent.png +0 -0
  78. jupyter-agent/ds-system-prompt.txt +0 -154
  79. jupyter-agent/jupyter-agent.png +0 -0
  80. jupyter-agent/llama3_template.jinja +0 -123
  81. jupyter-agent/requirements.txt +0 -9
  82. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
  83. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
  84. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
  85. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
  86. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
  87. jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
  88. jupyter-agent/utils.py +0 -409
  89. runbooks/aws/__init__.py +0 -58
  90. runbooks/aws/dynamodb_operations.py +0 -231
  91. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  92. runbooks/aws/ec2_describe_instances.py +0 -202
  93. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  94. runbooks/aws/ec2_run_instances.py +0 -213
  95. runbooks/aws/ec2_start_stop_instances.py +0 -212
  96. runbooks/aws/ec2_terminate_instances.py +0 -143
  97. runbooks/aws/ec2_unused_eips.py +0 -196
  98. runbooks/aws/ec2_unused_volumes.py +0 -188
  99. runbooks/aws/s3_create_bucket.py +0 -142
  100. runbooks/aws/s3_list_buckets.py +0 -152
  101. runbooks/aws/s3_list_objects.py +0 -156
  102. runbooks/aws/s3_object_operations.py +0 -183
  103. runbooks/aws/tagging_lambda_handler.py +0 -183
  104. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  105. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  106. runbooks/inventory/aws_organization.png +0 -0
  107. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  108. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  109. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  110. runbooks/inventory/update_aws_actions.py +0 -173
  111. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  112. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  113. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  114. runbooks/inventory/update_s3_public_access_block.py +0 -539
  115. runbooks/organizations/__init__.py +0 -12
  116. runbooks/organizations/manager.py +0 -374
  117. runbooks-0.7.0.dist-info/METADATA +0 -375
  118. /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
  119. /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
  120. /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
  121. /runbooks/inventory/{tests → Tests}/setup.py +0 -0
  122. /runbooks/inventory/{tests → Tests}/src.py +0 -0
  123. /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
  124. /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
  125. /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
  126. /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
  127. /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
  128. /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
  129. /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
  130. /runbooks/{aws → operate}/tags.json +0 -0
  131. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
  132. {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