runbooks 0.6.1__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 (142) hide show
  1. jupyter-agent/.env +2 -0
  2. jupyter-agent/.gradio/certificate.pem +31 -0
  3. jupyter-agent/__main__.log +8 -0
  4. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
  5. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
  6. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
  7. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
  8. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
  9. jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
  10. runbooks/__init__.py +87 -37
  11. runbooks/cfat/README.md +300 -49
  12. runbooks/cfat/__init__.py +2 -2
  13. runbooks/finops/README.md +337 -0
  14. runbooks/finops/__init__.py +2 -4
  15. runbooks/finops/cli.py +1 -1
  16. runbooks/inventory/aws_organization.png +0 -0
  17. runbooks/inventory/collectors/__init__.py +8 -0
  18. runbooks/inventory/collectors/aws_management.py +791 -0
  19. runbooks/inventory/collectors/aws_networking.py +3 -3
  20. runbooks/main.py +3416 -590
  21. runbooks/operate/__init__.py +207 -0
  22. runbooks/operate/base.py +311 -0
  23. runbooks/operate/cloudformation_operations.py +619 -0
  24. runbooks/operate/cloudwatch_operations.py +496 -0
  25. runbooks/operate/dynamodb_operations.py +812 -0
  26. runbooks/operate/ec2_operations.py +926 -0
  27. runbooks/operate/iam_operations.py +569 -0
  28. runbooks/operate/s3_operations.py +1211 -0
  29. runbooks/operate/tagging_operations.py +655 -0
  30. runbooks/remediation/CLAUDE.md +100 -0
  31. runbooks/remediation/DOME9.md +218 -0
  32. runbooks/remediation/README.md +26 -0
  33. runbooks/remediation/Tests/update_policy.py +74 -0
  34. runbooks/remediation/__init__.py +95 -0
  35. runbooks/remediation/acm_cert_expired_unused.py +98 -0
  36. runbooks/remediation/acm_remediation.py +875 -0
  37. runbooks/remediation/api_gateway_list.py +167 -0
  38. runbooks/remediation/base.py +643 -0
  39. runbooks/remediation/cloudtrail_remediation.py +908 -0
  40. runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
  41. runbooks/remediation/cognito_active_users.py +78 -0
  42. runbooks/remediation/cognito_remediation.py +856 -0
  43. runbooks/remediation/cognito_user_password_reset.py +163 -0
  44. runbooks/remediation/commons.py +455 -0
  45. runbooks/remediation/dynamodb_optimize.py +155 -0
  46. runbooks/remediation/dynamodb_remediation.py +744 -0
  47. runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
  48. runbooks/remediation/ec2_public_ips.py +134 -0
  49. runbooks/remediation/ec2_remediation.py +892 -0
  50. runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
  51. runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
  52. runbooks/remediation/ec2_unused_security_groups.py +202 -0
  53. runbooks/remediation/kms_enable_key_rotation.py +651 -0
  54. runbooks/remediation/kms_remediation.py +717 -0
  55. runbooks/remediation/lambda_list.py +243 -0
  56. runbooks/remediation/lambda_remediation.py +971 -0
  57. runbooks/remediation/multi_account.py +569 -0
  58. runbooks/remediation/rds_instance_list.py +199 -0
  59. runbooks/remediation/rds_remediation.py +873 -0
  60. runbooks/remediation/rds_snapshot_list.py +192 -0
  61. runbooks/remediation/requirements.txt +118 -0
  62. runbooks/remediation/s3_block_public_access.py +159 -0
  63. runbooks/remediation/s3_bucket_public_access.py +143 -0
  64. runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
  65. runbooks/remediation/s3_downloader.py +215 -0
  66. runbooks/remediation/s3_enable_access_logging.py +562 -0
  67. runbooks/remediation/s3_encryption.py +526 -0
  68. runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
  69. runbooks/remediation/s3_list.py +141 -0
  70. runbooks/remediation/s3_object_search.py +201 -0
  71. runbooks/remediation/s3_remediation.py +816 -0
  72. runbooks/remediation/scan_for_phrase.py +425 -0
  73. runbooks/remediation/workspaces_list.py +220 -0
  74. runbooks/{security_baseline → security}/README.md +191 -68
  75. runbooks/security/__init__.py +70 -0
  76. runbooks/{security_baseline → security}/security_baseline_tester.py +5 -3
  77. runbooks-0.7.5.dist-info/METADATA +606 -0
  78. {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/RECORD +115 -75
  79. {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/entry_points.txt +0 -1
  80. runbooks/aws/__init__.py +0 -58
  81. runbooks/aws/dynamodb_operations.py +0 -231
  82. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  83. runbooks/aws/ec2_describe_instances.py +0 -202
  84. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  85. runbooks/aws/ec2_run_instances.py +0 -213
  86. runbooks/aws/ec2_start_stop_instances.py +0 -212
  87. runbooks/aws/ec2_terminate_instances.py +0 -143
  88. runbooks/aws/ec2_unused_eips.py +0 -196
  89. runbooks/aws/ec2_unused_volumes.py +0 -188
  90. runbooks/aws/s3_create_bucket.py +0 -142
  91. runbooks/aws/s3_list_buckets.py +0 -152
  92. runbooks/aws/s3_list_objects.py +0 -156
  93. runbooks/aws/s3_object_operations.py +0 -183
  94. runbooks/aws/tagging_lambda_handler.py +0 -183
  95. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  96. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  97. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  98. runbooks/inventory/update_aws_actions.py +0 -173
  99. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  100. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  101. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  102. runbooks/inventory/update_s3_public_access_block.py +0 -539
  103. runbooks/organizations/__init__.py +0 -12
  104. runbooks/organizations/manager.py +0 -374
  105. runbooks/security_baseline/requirements.txt +0 -7
  106. runbooks-0.6.1.dist-info/METADATA +0 -373
  107. /runbooks/{aws → operate}/tags.json +0 -0
  108. /runbooks/{security_baseline → remediation/Tests}/__init__.py +0 -0
  109. /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
  110. /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
  111. /runbooks/{security_baseline → security}/checklist/alternate_contacts.py +0 -0
  112. /runbooks/{security_baseline → security}/checklist/bucket_public_access.py +0 -0
  113. /runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +0 -0
  114. /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
  115. /runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +0 -0
  116. /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
  117. /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
  118. /runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +0 -0
  119. /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
  120. /runbooks/{security_baseline → security}/checklist/root_access_key.py +0 -0
  121. /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
  122. /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
  123. /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
  124. /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
  125. /runbooks/{security_baseline → security}/config-origin.json +0 -0
  126. /runbooks/{security_baseline → security}/config.json +0 -0
  127. /runbooks/{security_baseline → security}/permission.json +0 -0
  128. /runbooks/{security_baseline → security}/report_generator.py +0 -0
  129. /runbooks/{security_baseline → security}/report_template_en.html +0 -0
  130. /runbooks/{security_baseline → security}/report_template_jp.html +0 -0
  131. /runbooks/{security_baseline → security}/report_template_kr.html +0 -0
  132. /runbooks/{security_baseline → security}/report_template_vn.html +0 -0
  133. /runbooks/{security_baseline → security}/run_script.py +0 -0
  134. /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
  135. /runbooks/{security_baseline → security}/utils/common.py +0 -0
  136. /runbooks/{security_baseline → security}/utils/enums.py +0 -0
  137. /runbooks/{security_baseline → security}/utils/language.py +0 -0
  138. /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
  139. /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
  140. {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/WHEEL +0 -0
  141. {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/licenses/LICENSE +0 -0
  142. {runbooks-0.6.1.dist-info → runbooks-0.7.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,496 @@
1
+ """
2
+ CloudWatch Operations Module.
3
+
4
+ Provides comprehensive CloudWatch resource management capabilities including
5
+ log group management, retention policies, and monitoring configuration.
6
+
7
+ Migrated and enhanced from:
8
+ - inventory/update_cloudwatch_logs_retention_policy.py
9
+ """
10
+
11
+ import json
12
+ from datetime import datetime
13
+ from typing import Any, Dict, List, Optional, Union
14
+
15
+ import boto3
16
+ from botocore.exceptions import ClientError
17
+ from loguru import logger
18
+
19
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
20
+
21
+
22
+ class CloudWatchOperations(BaseOperation):
23
+ """
24
+ CloudWatch resource operations and lifecycle management.
25
+
26
+ Handles all CloudWatch-related operational tasks including log group management,
27
+ retention policy updates, and monitoring configuration.
28
+ """
29
+
30
+ service_name = "cloudwatch"
31
+ supported_operations = {
32
+ "create_log_group",
33
+ "delete_log_group",
34
+ "update_log_retention_policy",
35
+ "create_metric_alarm",
36
+ "delete_metric_alarm",
37
+ "put_metric_data",
38
+ "create_dashboard",
39
+ "delete_dashboard",
40
+ "tag_log_group",
41
+ "untag_log_group",
42
+ }
43
+ requires_confirmation = True
44
+
45
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
46
+ """Initialize CloudWatch operations."""
47
+ super().__init__(profile, region, dry_run)
48
+
49
+ def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
50
+ """
51
+ Execute CloudWatch operation.
52
+
53
+ Args:
54
+ context: Operation context
55
+ operation_type: Type of operation to execute
56
+ **kwargs: Operation-specific arguments
57
+
58
+ Returns:
59
+ List of operation results
60
+ """
61
+ self.validate_context(context)
62
+
63
+ if operation_type == "create_log_group":
64
+ return self.create_log_group(context, **kwargs)
65
+ elif operation_type == "delete_log_group":
66
+ return self.delete_log_group(context, kwargs.get("log_group_name"))
67
+ elif operation_type == "update_log_retention_policy":
68
+ return self.update_log_retention_policy(context, **kwargs)
69
+ elif operation_type == "create_metric_alarm":
70
+ return self.create_metric_alarm(context, **kwargs)
71
+ elif operation_type == "delete_metric_alarm":
72
+ return self.delete_metric_alarm(context, kwargs.get("alarm_name"))
73
+ elif operation_type == "put_metric_data":
74
+ return self.put_metric_data(context, **kwargs)
75
+ elif operation_type == "create_dashboard":
76
+ return self.create_dashboard(context, **kwargs)
77
+ elif operation_type == "delete_dashboard":
78
+ return self.delete_dashboard(context, kwargs.get("dashboard_name"))
79
+ elif operation_type == "tag_log_group":
80
+ return self.tag_log_group(context, **kwargs)
81
+ elif operation_type == "untag_log_group":
82
+ return self.untag_log_group(context, **kwargs)
83
+ else:
84
+ raise ValueError(f"Unsupported operation: {operation_type}")
85
+
86
+ def create_log_group(
87
+ self,
88
+ context: OperationContext,
89
+ log_group_name: str,
90
+ kms_key_id: Optional[str] = None,
91
+ tags: Optional[Dict[str, str]] = None,
92
+ retention_in_days: Optional[int] = None,
93
+ ) -> List[OperationResult]:
94
+ """
95
+ Create CloudWatch log group.
96
+
97
+ Args:
98
+ context: Operation context
99
+ log_group_name: Name of log group to create
100
+ kms_key_id: KMS key ID for encryption
101
+ tags: Log group tags
102
+ retention_in_days: Log retention period in days
103
+
104
+ Returns:
105
+ List of operation results
106
+ """
107
+ logs_client = self.get_client("logs", context.region)
108
+
109
+ result = self.create_operation_result(context, "create_log_group", "logs:log_group", log_group_name)
110
+
111
+ try:
112
+ if context.dry_run:
113
+ logger.info(f"[DRY-RUN] Would create log group {log_group_name}")
114
+ result.mark_completed(OperationStatus.DRY_RUN)
115
+ return [result]
116
+
117
+ create_params = {"logGroupName": log_group_name}
118
+
119
+ if kms_key_id:
120
+ create_params["kmsKeyId"] = kms_key_id
121
+ if tags:
122
+ create_params["tags"] = tags
123
+
124
+ response = self.execute_aws_call(logs_client, "create_log_group", **create_params)
125
+
126
+ # Set retention policy if specified
127
+ if retention_in_days:
128
+ self.execute_aws_call(
129
+ logs_client, "put_retention_policy", logGroupName=log_group_name, retentionInDays=retention_in_days
130
+ )
131
+ logger.info(f"Set retention policy to {retention_in_days} days for {log_group_name}")
132
+
133
+ result.response_data = response
134
+ result.mark_completed(OperationStatus.SUCCESS)
135
+ logger.info(f"Successfully created log group {log_group_name}")
136
+
137
+ except ClientError as e:
138
+ error_msg = f"Failed to create log group {log_group_name}: {e}"
139
+ logger.error(error_msg)
140
+ result.mark_completed(OperationStatus.FAILED, error_msg)
141
+
142
+ return [result]
143
+
144
+ def delete_log_group(self, context: OperationContext, log_group_name: str) -> List[OperationResult]:
145
+ """
146
+ Delete CloudWatch log group.
147
+
148
+ Args:
149
+ context: Operation context
150
+ log_group_name: Name of log group to delete
151
+
152
+ Returns:
153
+ List of operation results
154
+ """
155
+ logs_client = self.get_client("logs", context.region)
156
+
157
+ result = self.create_operation_result(context, "delete_log_group", "logs:log_group", log_group_name)
158
+
159
+ try:
160
+ if not self.confirm_operation(context, log_group_name, "delete log group"):
161
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
162
+ return [result]
163
+
164
+ if context.dry_run:
165
+ logger.info(f"[DRY-RUN] Would delete log group {log_group_name}")
166
+ result.mark_completed(OperationStatus.DRY_RUN)
167
+ else:
168
+ response = self.execute_aws_call(logs_client, "delete_log_group", logGroupName=log_group_name)
169
+
170
+ result.response_data = response
171
+ result.mark_completed(OperationStatus.SUCCESS)
172
+ logger.info(f"Successfully deleted log group {log_group_name}")
173
+
174
+ except ClientError as e:
175
+ error_msg = f"Failed to delete log group {log_group_name}: {e}"
176
+ logger.error(error_msg)
177
+ result.mark_completed(OperationStatus.FAILED, error_msg)
178
+
179
+ return [result]
180
+
181
+ def update_log_retention_policy(
182
+ self,
183
+ context: OperationContext,
184
+ log_group_name: Optional[str] = None,
185
+ retention_in_days: int = 30,
186
+ update_all_log_groups: bool = False,
187
+ ) -> List[OperationResult]:
188
+ """
189
+ Update CloudWatch log retention policy.
190
+
191
+ Migrated from inventory/update_cloudwatch_logs_retention_policy.py
192
+
193
+ Args:
194
+ context: Operation context
195
+ log_group_name: Specific log group name (if None, updates all)
196
+ retention_in_days: Retention period in days
197
+ update_all_log_groups: Whether to update all log groups
198
+
199
+ Returns:
200
+ List of operation results
201
+ """
202
+ logs_client = self.get_client("logs", context.region)
203
+ results = []
204
+
205
+ try:
206
+ if log_group_name:
207
+ # Update specific log group
208
+ log_groups = [log_group_name]
209
+ elif update_all_log_groups:
210
+ # Get all log groups
211
+ paginator = logs_client.get_paginator("describe_log_groups")
212
+ log_groups = []
213
+
214
+ for page in paginator.paginate():
215
+ for log_group in page["logGroups"]:
216
+ log_groups.append(log_group["logGroupName"])
217
+ else:
218
+ raise ValueError("Either log_group_name must be specified or update_all_log_groups must be True")
219
+
220
+ for lg_name in log_groups:
221
+ result = self.create_operation_result(context, "update_log_retention_policy", "logs:log_group", lg_name)
222
+
223
+ try:
224
+ if context.dry_run:
225
+ logger.info(f"[DRY-RUN] Would set retention to {retention_in_days} days for {lg_name}")
226
+ result.mark_completed(OperationStatus.DRY_RUN)
227
+ else:
228
+ response = self.execute_aws_call(
229
+ logs_client, "put_retention_policy", logGroupName=lg_name, retentionInDays=retention_in_days
230
+ )
231
+
232
+ result.response_data = response
233
+ result.mark_completed(OperationStatus.SUCCESS)
234
+ logger.info(f"Successfully updated retention policy for {lg_name}")
235
+
236
+ except ClientError as e:
237
+ error_msg = f"Failed to update retention policy for {lg_name}: {e}"
238
+ logger.error(error_msg)
239
+ result.mark_completed(OperationStatus.FAILED, error_msg)
240
+
241
+ results.append(result)
242
+
243
+ except Exception as e:
244
+ error_msg = f"Failed to update log retention policies: {e}"
245
+ logger.error(error_msg)
246
+ result = self.create_operation_result(
247
+ context, "update_log_retention_policy", "logs:operation", "batch_update"
248
+ )
249
+ result.mark_completed(OperationStatus.FAILED, error_msg)
250
+ results.append(result)
251
+
252
+ return results
253
+
254
+ def create_metric_alarm(
255
+ self,
256
+ context: OperationContext,
257
+ alarm_name: str,
258
+ comparison_operator: str,
259
+ evaluation_periods: int,
260
+ metric_name: str,
261
+ namespace: str,
262
+ period: int,
263
+ statistic: str,
264
+ threshold: float,
265
+ actions_enabled: bool = True,
266
+ alarm_actions: Optional[List[str]] = None,
267
+ alarm_description: Optional[str] = None,
268
+ dimensions: Optional[List[Dict[str, str]]] = None,
269
+ insufficient_data_actions: Optional[List[str]] = None,
270
+ ok_actions: Optional[List[str]] = None,
271
+ tags: Optional[List[Dict[str, str]]] = None,
272
+ unit: Optional[str] = None,
273
+ ) -> List[OperationResult]:
274
+ """
275
+ Create CloudWatch metric alarm.
276
+
277
+ Args:
278
+ context: Operation context
279
+ alarm_name: Name of alarm to create
280
+ comparison_operator: Comparison operator
281
+ evaluation_periods: Number of evaluation periods
282
+ metric_name: Metric name
283
+ namespace: Metric namespace
284
+ period: Period in seconds
285
+ statistic: Statistic type
286
+ threshold: Alarm threshold
287
+ actions_enabled: Whether actions are enabled
288
+ alarm_actions: Alarm actions
289
+ alarm_description: Alarm description
290
+ dimensions: Metric dimensions
291
+ insufficient_data_actions: Insufficient data actions
292
+ ok_actions: OK actions
293
+ tags: Alarm tags
294
+ unit: Metric unit
295
+
296
+ Returns:
297
+ List of operation results
298
+ """
299
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
300
+
301
+ result = self.create_operation_result(context, "create_metric_alarm", "cloudwatch:alarm", alarm_name)
302
+
303
+ try:
304
+ if context.dry_run:
305
+ logger.info(f"[DRY-RUN] Would create CloudWatch alarm {alarm_name}")
306
+ result.mark_completed(OperationStatus.DRY_RUN)
307
+ return [result]
308
+
309
+ alarm_params = {
310
+ "AlarmName": alarm_name,
311
+ "ComparisonOperator": comparison_operator,
312
+ "EvaluationPeriods": evaluation_periods,
313
+ "MetricName": metric_name,
314
+ "Namespace": namespace,
315
+ "Period": period,
316
+ "Statistic": statistic,
317
+ "Threshold": threshold,
318
+ "ActionsEnabled": actions_enabled,
319
+ }
320
+
321
+ if alarm_actions:
322
+ alarm_params["AlarmActions"] = alarm_actions
323
+ if alarm_description:
324
+ alarm_params["AlarmDescription"] = alarm_description
325
+ if dimensions:
326
+ alarm_params["Dimensions"] = dimensions
327
+ if insufficient_data_actions:
328
+ alarm_params["InsufficientDataActions"] = insufficient_data_actions
329
+ if ok_actions:
330
+ alarm_params["OKActions"] = ok_actions
331
+ if tags:
332
+ alarm_params["Tags"] = tags
333
+ if unit:
334
+ alarm_params["Unit"] = unit
335
+
336
+ response = self.execute_aws_call(cloudwatch_client, "put_metric_alarm", **alarm_params)
337
+
338
+ result.response_data = response
339
+ result.mark_completed(OperationStatus.SUCCESS)
340
+ logger.info(f"Successfully created CloudWatch alarm {alarm_name}")
341
+
342
+ except ClientError as e:
343
+ error_msg = f"Failed to create CloudWatch alarm {alarm_name}: {e}"
344
+ logger.error(error_msg)
345
+ result.mark_completed(OperationStatus.FAILED, error_msg)
346
+
347
+ return [result]
348
+
349
+ def delete_metric_alarm(self, context: OperationContext, alarm_name: str) -> List[OperationResult]:
350
+ """
351
+ Delete CloudWatch metric alarm.
352
+
353
+ Args:
354
+ context: Operation context
355
+ alarm_name: Name of alarm to delete
356
+
357
+ Returns:
358
+ List of operation results
359
+ """
360
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
361
+
362
+ result = self.create_operation_result(context, "delete_metric_alarm", "cloudwatch:alarm", alarm_name)
363
+
364
+ try:
365
+ if not self.confirm_operation(context, alarm_name, "delete CloudWatch alarm"):
366
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
367
+ return [result]
368
+
369
+ if context.dry_run:
370
+ logger.info(f"[DRY-RUN] Would delete CloudWatch alarm {alarm_name}")
371
+ result.mark_completed(OperationStatus.DRY_RUN)
372
+ else:
373
+ response = self.execute_aws_call(cloudwatch_client, "delete_alarms", AlarmNames=[alarm_name])
374
+
375
+ result.response_data = response
376
+ result.mark_completed(OperationStatus.SUCCESS)
377
+ logger.info(f"Successfully deleted CloudWatch alarm {alarm_name}")
378
+
379
+ except ClientError as e:
380
+ error_msg = f"Failed to delete CloudWatch alarm {alarm_name}: {e}"
381
+ logger.error(error_msg)
382
+ result.mark_completed(OperationStatus.FAILED, error_msg)
383
+
384
+ return [result]
385
+
386
+ def put_metric_data(
387
+ self, context: OperationContext, namespace: str, metric_data: List[Dict[str, Any]]
388
+ ) -> List[OperationResult]:
389
+ """
390
+ Put custom metric data to CloudWatch.
391
+
392
+ Args:
393
+ context: Operation context
394
+ namespace: Metric namespace
395
+ metric_data: List of metric data points
396
+
397
+ Returns:
398
+ List of operation results
399
+ """
400
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
401
+
402
+ result = self.create_operation_result(context, "put_metric_data", "cloudwatch:metric", namespace)
403
+
404
+ try:
405
+ if context.dry_run:
406
+ logger.info(f"[DRY-RUN] Would put {len(metric_data)} metric data points to {namespace}")
407
+ result.mark_completed(OperationStatus.DRY_RUN)
408
+ else:
409
+ response = self.execute_aws_call(
410
+ cloudwatch_client, "put_metric_data", Namespace=namespace, MetricData=metric_data
411
+ )
412
+
413
+ result.response_data = response
414
+ result.mark_completed(OperationStatus.SUCCESS)
415
+ logger.info(f"Successfully put {len(metric_data)} metric data points to {namespace}")
416
+
417
+ except ClientError as e:
418
+ error_msg = f"Failed to put metric data to {namespace}: {e}"
419
+ logger.error(error_msg)
420
+ result.mark_completed(OperationStatus.FAILED, error_msg)
421
+
422
+ return [result]
423
+
424
+ def tag_log_group(
425
+ self, context: OperationContext, log_group_name: str, tags: Dict[str, str]
426
+ ) -> List[OperationResult]:
427
+ """
428
+ Add tags to CloudWatch log group.
429
+
430
+ Args:
431
+ context: Operation context
432
+ log_group_name: Name of log group to tag
433
+ tags: Tags to add
434
+
435
+ Returns:
436
+ List of operation results
437
+ """
438
+ logs_client = self.get_client("logs", context.region)
439
+
440
+ result = self.create_operation_result(context, "tag_log_group", "logs:log_group", log_group_name)
441
+
442
+ try:
443
+ if context.dry_run:
444
+ logger.info(f"[DRY-RUN] Would add {len(tags)} tags to log group {log_group_name}")
445
+ result.mark_completed(OperationStatus.DRY_RUN)
446
+ else:
447
+ response = self.execute_aws_call(logs_client, "tag_log_group", logGroupName=log_group_name, tags=tags)
448
+
449
+ result.response_data = response
450
+ result.mark_completed(OperationStatus.SUCCESS)
451
+ logger.info(f"Successfully added {len(tags)} tags to log group {log_group_name}")
452
+
453
+ except ClientError as e:
454
+ error_msg = f"Failed to tag log group {log_group_name}: {e}"
455
+ logger.error(error_msg)
456
+ result.mark_completed(OperationStatus.FAILED, error_msg)
457
+
458
+ return [result]
459
+
460
+ def untag_log_group(
461
+ self, context: OperationContext, log_group_name: str, tag_keys: List[str]
462
+ ) -> List[OperationResult]:
463
+ """
464
+ Remove tags from CloudWatch log group.
465
+
466
+ Args:
467
+ context: Operation context
468
+ log_group_name: Name of log group to untag
469
+ tag_keys: Tag keys to remove
470
+
471
+ Returns:
472
+ List of operation results
473
+ """
474
+ logs_client = self.get_client("logs", context.region)
475
+
476
+ result = self.create_operation_result(context, "untag_log_group", "logs:log_group", log_group_name)
477
+
478
+ try:
479
+ if context.dry_run:
480
+ logger.info(f"[DRY-RUN] Would remove {len(tag_keys)} tags from log group {log_group_name}")
481
+ result.mark_completed(OperationStatus.DRY_RUN)
482
+ else:
483
+ response = self.execute_aws_call(
484
+ logs_client, "untag_log_group", logGroupName=log_group_name, tags=tag_keys
485
+ )
486
+
487
+ result.response_data = response
488
+ result.mark_completed(OperationStatus.SUCCESS)
489
+ logger.info(f"Successfully removed {len(tag_keys)} tags from log group {log_group_name}")
490
+
491
+ except ClientError as e:
492
+ error_msg = f"Failed to untag log group {log_group_name}: {e}"
493
+ logger.error(error_msg)
494
+ result.mark_completed(OperationStatus.FAILED, error_msg)
495
+
496
+ return [result]