runbooks 1.1.4__py3-none-any.whl → 1.1.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 (273) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -361,45 +361,45 @@ class IAMOperations(BaseOperation):
361
361
  return [result]
362
362
 
363
363
  def update_role(
364
- self,
365
- context: OperationContext,
364
+ self,
365
+ context: OperationContext,
366
366
  role_name: str,
367
367
  description: Optional[str] = None,
368
368
  max_session_duration: Optional[int] = None,
369
- permissions_boundary: Optional[str] = None
369
+ permissions_boundary: Optional[str] = None,
370
370
  ) -> List[OperationResult]:
371
371
  """
372
372
  Update IAM role properties.
373
-
373
+
374
374
  Args:
375
375
  context: Operation context
376
376
  role_name: Name of role to update
377
377
  description: New description
378
378
  max_session_duration: New max session duration
379
379
  permissions_boundary: New permissions boundary ARN
380
-
380
+
381
381
  Returns:
382
382
  List of operation results
383
383
  """
384
384
  iam_client = self.get_client("iam")
385
-
385
+
386
386
  result = self.create_operation_result(context, "update_role", "iam:role", role_name)
387
-
387
+
388
388
  try:
389
389
  if context.dry_run:
390
390
  logger.info(f"[DRY-RUN] Would update IAM role {role_name}")
391
391
  result.mark_completed(OperationStatus.DRY_RUN)
392
392
  return [result]
393
-
393
+
394
394
  update_params = {"RoleName": role_name}
395
-
395
+
396
396
  if description is not None:
397
397
  update_params["Description"] = description
398
398
  if max_session_duration is not None:
399
399
  update_params["MaxSessionDuration"] = max_session_duration
400
-
400
+
401
401
  response = self.execute_aws_call(iam_client, "update_role", **update_params)
402
-
402
+
403
403
  # Handle permissions boundary separately if provided
404
404
  if permissions_boundary is not None:
405
405
  if permissions_boundary == "":
@@ -407,66 +407,66 @@ class IAMOperations(BaseOperation):
407
407
  self.execute_aws_call(iam_client, "delete_role_permissions_boundary", RoleName=role_name)
408
408
  else:
409
409
  # Set permissions boundary
410
- self.execute_aws_call(iam_client, "put_role_permissions_boundary",
411
- RoleName=role_name, PermissionsBoundary=permissions_boundary)
412
-
410
+ self.execute_aws_call(
411
+ iam_client,
412
+ "put_role_permissions_boundary",
413
+ RoleName=role_name,
414
+ PermissionsBoundary=permissions_boundary,
415
+ )
416
+
413
417
  result.response_data = response
414
418
  result.mark_completed(OperationStatus.SUCCESS)
415
419
  logger.info(f"Successfully updated IAM role {role_name}")
416
-
420
+
417
421
  except ClientError as e:
418
422
  error_msg = f"Failed to update IAM role {role_name}: {e}"
419
423
  logger.error(error_msg)
420
424
  result.mark_completed(OperationStatus.FAILED, error_msg)
421
-
425
+
422
426
  return [result]
423
427
 
424
428
  def update_policy(
425
- self,
426
- context: OperationContext,
427
- policy_arn: str,
428
- policy_document: str,
429
- set_as_default: bool = True
429
+ self, context: OperationContext, policy_arn: str, policy_document: str, set_as_default: bool = True
430
430
  ) -> List[OperationResult]:
431
431
  """
432
432
  Update IAM policy by creating a new version.
433
-
433
+
434
434
  Args:
435
435
  context: Operation context
436
436
  policy_arn: ARN of policy to update
437
437
  policy_document: New policy document JSON
438
438
  set_as_default: Whether to set new version as default
439
-
439
+
440
440
  Returns:
441
441
  List of operation results
442
442
  """
443
443
  iam_client = self.get_client("iam")
444
-
444
+
445
445
  result = self.create_operation_result(context, "update_policy", "iam:policy", policy_arn)
446
-
446
+
447
447
  try:
448
448
  if context.dry_run:
449
449
  logger.info(f"[DRY-RUN] Would update IAM policy {policy_arn}")
450
450
  result.mark_completed(OperationStatus.DRY_RUN)
451
451
  return [result]
452
-
452
+
453
453
  response = self.execute_aws_call(
454
454
  iam_client,
455
455
  "create_policy_version",
456
456
  PolicyArn=policy_arn,
457
457
  PolicyDocument=policy_document,
458
- SetAsDefault=set_as_default
458
+ SetAsDefault=set_as_default,
459
459
  )
460
-
460
+
461
461
  result.response_data = response
462
462
  result.mark_completed(OperationStatus.SUCCESS)
463
463
  logger.info(f"Successfully updated IAM policy {policy_arn}")
464
-
464
+
465
465
  except ClientError as e:
466
466
  error_msg = f"Failed to update IAM policy {policy_arn}: {e}"
467
467
  logger.error(error_msg)
468
468
  result.mark_completed(OperationStatus.FAILED, error_msg)
469
-
469
+
470
470
  return [result]
471
471
 
472
472
  def create_service_linked_role(
@@ -474,82 +474,84 @@ class IAMOperations(BaseOperation):
474
474
  context: OperationContext,
475
475
  aws_service_name: str,
476
476
  description: Optional[str] = None,
477
- custom_suffix: Optional[str] = None
477
+ custom_suffix: Optional[str] = None,
478
478
  ) -> List[OperationResult]:
479
479
  """
480
480
  Create service-linked role for AWS service.
481
-
481
+
482
482
  Args:
483
483
  context: Operation context
484
484
  aws_service_name: AWS service name (e.g., 'elasticloadbalancing.amazonaws.com')
485
485
  description: Custom description
486
486
  custom_suffix: Custom suffix for role name
487
-
487
+
488
488
  Returns:
489
489
  List of operation results
490
490
  """
491
491
  iam_client = self.get_client("iam")
492
-
493
- result = self.create_operation_result(context, "create_service_linked_role", "iam:service-linked-role", aws_service_name)
494
-
492
+
493
+ result = self.create_operation_result(
494
+ context, "create_service_linked_role", "iam:service-linked-role", aws_service_name
495
+ )
496
+
495
497
  try:
496
498
  if context.dry_run:
497
499
  logger.info(f"[DRY-RUN] Would create service-linked role for {aws_service_name}")
498
500
  result.mark_completed(OperationStatus.DRY_RUN)
499
501
  return [result]
500
-
502
+
501
503
  create_params = {"AWSServiceName": aws_service_name}
502
-
504
+
503
505
  if description:
504
506
  create_params["Description"] = description
505
507
  if custom_suffix:
506
508
  create_params["CustomSuffix"] = custom_suffix
507
-
509
+
508
510
  response = self.execute_aws_call(iam_client, "create_service_linked_role", **create_params)
509
-
511
+
510
512
  result.response_data = response
511
513
  result.mark_completed(OperationStatus.SUCCESS)
512
514
  logger.info(f"Successfully created service-linked role for {aws_service_name}")
513
-
515
+
514
516
  except ClientError as e:
515
517
  error_msg = f"Failed to create service-linked role for {aws_service_name}: {e}"
516
518
  logger.error(error_msg)
517
519
  result.mark_completed(OperationStatus.FAILED, error_msg)
518
-
520
+
519
521
  return [result]
520
522
 
521
523
  def untag_role(self, context: OperationContext, role_name: str, tag_keys: List[str]) -> List[OperationResult]:
522
524
  """
523
525
  Remove tags from IAM role.
524
-
526
+
525
527
  Args:
526
528
  context: Operation context
527
529
  role_name: Name of role to untag
528
530
  tag_keys: List of tag keys to remove
529
-
531
+
530
532
  Returns:
531
533
  List of operation results
532
534
  """
533
535
  iam_client = self.get_client("iam")
534
-
536
+
535
537
  result = self.create_operation_result(context, "untag_role", "iam:role", role_name)
536
-
538
+
537
539
  try:
538
540
  if context.dry_run:
539
541
  logger.info(f"[DRY-RUN] Would remove {len(tag_keys)} tags from role {role_name}")
540
542
  result.mark_completed(OperationStatus.DRY_RUN)
541
543
  else:
542
544
  response = self.execute_aws_call(iam_client, "untag_role", RoleName=role_name, TagKeys=tag_keys)
543
-
545
+
544
546
  result.response_data = response
545
547
  result.mark_completed(OperationStatus.SUCCESS)
546
548
  logger.info(f"Successfully removed {len(tag_keys)} tags from role {role_name}")
547
-
549
+
548
550
  except ClientError as e:
549
551
  error_msg = f"Failed to untag role {role_name}: {e}"
550
552
  logger.error(error_msg)
551
553
  result.mark_completed(OperationStatus.FAILED, error_msg)
552
-
554
+
553
555
  return [result]
554
556
 
555
557
  def attach_role_policy(self, context: OperationContext, role_name: str, policy_arn: str) -> List[OperationResult]:
@@ -791,69 +793,68 @@ class IAMOperations(BaseOperation):
791
793
  @dataclass
792
794
  class ExpiringAccessKey:
793
795
  """Data class for expiring access key information."""
796
+
794
797
  username: str
795
798
  access_key_id: str
796
799
  create_date: datetime
797
800
  days_old: int
798
801
 
799
- def list_expiring_access_keys(
800
- self, context: OperationContext, threshold_days: int = 90
801
- ) -> List[OperationResult]:
802
+ def list_expiring_access_keys(self, context: OperationContext, threshold_days: int = 90) -> List[OperationResult]:
802
803
  """
803
804
  List all IAM access keys that are expiring within threshold days.
804
-
805
+
805
806
  Migrated from unSkript aws_list_expiring_access_keys function.
806
-
807
+
807
808
  Args:
808
809
  context: Operation context
809
810
  threshold_days: Threshold number of days to check for expiry
810
-
811
+
811
812
  Returns:
812
813
  List of operation results with expiring access keys
813
814
  """
814
815
  iam_client = self.get_client("iam")
815
-
816
+
816
817
  result = self.create_operation_result(
817
818
  context, "list_expiring_access_keys", "iam:access-keys", f"threshold-{threshold_days}-days"
818
819
  )
819
-
820
+
820
821
  try:
821
822
  console.print(f"[blue]Checking for access keys older than {threshold_days} days...[/blue]")
822
-
823
+
823
824
  expiring_keys = []
824
-
825
+
825
826
  # Get all IAM users
826
- paginator = iam_client.get_paginator('list_users')
827
-
827
+ paginator = iam_client.get_paginator("list_users")
828
+
828
829
  for page in paginator.paginate():
829
- for user in page['Users']:
830
- username = user['UserName']
831
-
830
+ for user in page["Users"]:
831
+ username = user["UserName"]
832
+
832
833
  try:
833
834
  # List access keys for each user
834
835
  response = self.execute_aws_call(iam_client, "list_access_keys", UserName=username)
835
-
836
+
836
837
  for key_metadata in response.get("AccessKeyMetadata", []):
837
838
  create_date = key_metadata["CreateDate"]
838
839
  right_now = datetime.now(dateutil.tz.tzlocal())
839
-
840
+
840
841
  # Calculate age in days
841
842
  age_diff = right_now - create_date
842
843
  days_old = age_diff.days
843
-
844
+
844
845
  if days_old > threshold_days:
845
846
  expiring_key = self.ExpiringAccessKey(
846
847
  username=username,
847
848
  access_key_id=key_metadata["AccessKeyId"],
848
849
  create_date=create_date,
849
- days_old=days_old
850
+ days_old=days_old,
850
851
  )
851
852
  expiring_keys.append(expiring_key)
852
-
853
+
853
854
  except ClientError as e:
854
- if e.response['Error']['Code'] != 'NoSuchEntity':
855
+ if e.response["Error"]["Code"] != "NoSuchEntity":
855
856
  logger.warning(f"Failed to list access keys for user {username}: {e}")
856
-
857
+
857
858
  # Display results with Rich table
858
859
  if expiring_keys:
859
860
  table = Table(title=f"Access Keys Expiring (>{threshold_days} days old)")
@@ -861,85 +862,87 @@ class IAMOperations(BaseOperation):
861
862
  table.add_column("Access Key ID", style="yellow")
862
863
  table.add_column("Created Date", style="magenta")
863
864
  table.add_column("Days Old", style="red")
864
-
865
+
865
866
  for key in expiring_keys:
866
867
  table.add_row(
867
868
  key.username,
868
869
  key.access_key_id,
869
870
  key.create_date.strftime("%Y-%m-%d %H:%M:%S"),
870
- str(key.days_old)
871
+ str(key.days_old),
871
872
  )
872
-
873
+
873
874
  console.print(table)
874
875
  console.print(f"[red]Found {len(expiring_keys)} expiring access keys[/red]")
875
876
  else:
876
877
  console.print(Panel("[green]✅ No expiring access keys found[/green]", title="Success"))
877
-
878
+
878
879
  result.response_data = {
879
880
  "expiring_keys": [
880
881
  {
881
882
  "username": key.username,
882
883
  "access_key_id": key.access_key_id,
883
884
  "create_date": key.create_date.isoformat(),
884
- "days_old": key.days_old
885
+ "days_old": key.days_old,
885
886
  }
886
887
  for key in expiring_keys
887
888
  ],
888
889
  "count": len(expiring_keys),
889
- "threshold_days": threshold_days
890
+ "threshold_days": threshold_days,
890
891
  }
891
892
  result.mark_completed(OperationStatus.SUCCESS)
892
893
  logger.info(f"Found {len(expiring_keys)} expiring access keys")
893
-
894
+
894
895
  except Exception as e:
895
896
  error_msg = f"Failed to list expiring access keys: {e}"
896
897
  logger.error(error_msg)
897
898
  result.mark_completed(OperationStatus.FAILED, error_msg)
898
-
899
+
899
900
  return [result]
900
901
 
901
902
  def create_access_key(self, context: OperationContext, username: str) -> List[OperationResult]:
902
903
  """
903
904
  Create new access key for specified IAM user.
904
-
905
+
905
906
  Migrated from unSkript aws_create_access_key function.
906
-
907
+
907
908
  Args:
908
909
  context: Operation context
909
910
  username: IAM username to create access key for
910
-
911
+
911
912
  Returns:
912
913
  List of operation results
913
914
  """
914
915
  iam_client = self.get_client("iam")
915
-
916
+
916
917
  result = self.create_operation_result(context, "create_access_key", "iam:access-key", username)
917
-
918
+
918
919
  try:
919
920
  if context.dry_run:
920
921
  console.print(f"[yellow][DRY-RUN] Would create new access key for user {username}[/yellow]")
921
922
  result.mark_completed(OperationStatus.DRY_RUN)
922
923
  return [result]
923
-
924
+
924
925
  # Safety confirmation for access key creation
925
926
  if not self.confirm_operation(context, username, "create new access key for user"):
926
927
  result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
927
928
  return [result]
928
-
929
+
929
930
  response = self.execute_aws_call(iam_client, "create_access_key", UserName=username)
930
-
931
+
931
932
  # Extract access key information
932
933
  access_key = response.get("AccessKey", {})
933
-
934
+
934
935
  # Display new access key information (with security warning)
935
- console.print(Panel(
936
- f"[green]✅ New access key created for user: {username}[/green]\n"
937
- f"[yellow]⚠️ IMPORTANT: Save these credentials securely![/yellow]\n"
938
- f"Access Key ID: [cyan]{access_key.get('AccessKeyId')}[/cyan]\n"
939
- f"Secret Access Key: [red]{'*' * 20}[/red] (Check logs for full key)",
940
- title="Access Key Created"
941
- ))
942
-
936
+ console.print(
937
+ Panel(
938
+ f"[green] New access key created for user: {username}[/green]\n"
939
+ f"[yellow]⚠️ IMPORTANT: Save these credentials securely![/yellow]\n"
940
+ f"Access Key ID: [cyan]{access_key.get('AccessKeyId')}[/cyan]\n"
941
+ f"Secret Access Key: [red]{'*' * 20}[/red] (Check logs for full key)",
942
+ title="Access Key Created",
943
+ )
944
+ )
945
+
943
946
  result.response_data = {
944
947
  "username": username,
945
948
  "access_key_id": access_key.get("AccessKeyId"),
@@ -949,13 +952,13 @@ class IAMOperations(BaseOperation):
949
952
  }
950
953
  result.mark_completed(OperationStatus.SUCCESS)
951
954
  logger.info(f"Successfully created access key for user {username}")
952
-
955
+
953
956
  except ClientError as e:
954
957
  error_msg = f"Failed to create access key for user {username}: {e}"
955
958
  logger.error(error_msg)
956
959
  result.mark_completed(OperationStatus.FAILED, error_msg)
957
960
  console.print(f"[red]❌ {error_msg}[/red]")
958
-
961
+
959
962
  return [result]
960
963
 
961
964
  def update_access_key_status(
@@ -963,121 +966,114 @@ class IAMOperations(BaseOperation):
963
966
  ) -> List[OperationResult]:
964
967
  """
965
968
  Update access key status (Active/Inactive).
966
-
969
+
967
970
  Migrated from unSkript aws_update_access_key function.
968
-
971
+
969
972
  Args:
970
973
  context: Operation context
971
974
  username: IAM username
972
975
  access_key_id: Access key ID to update
973
976
  status: New status ('Active' or 'Inactive')
974
-
977
+
975
978
  Returns:
976
979
  List of operation results
977
980
  """
978
981
  iam_client = self.get_client("iam")
979
-
982
+
980
983
  result = self.create_operation_result(
981
984
  context, "update_access_key_status", "iam:access-key", f"{username}:{access_key_id}"
982
985
  )
983
-
986
+
984
987
  try:
985
- if status not in ['Active', 'Inactive']:
988
+ if status not in ["Active", "Inactive"]:
986
989
  raise ValueError(f"Invalid status '{status}'. Must be 'Active' or 'Inactive'")
987
-
990
+
988
991
  if context.dry_run:
989
992
  console.print(f"[yellow][DRY-RUN] Would update access key {access_key_id} status to {status}[/yellow]")
990
993
  result.mark_completed(OperationStatus.DRY_RUN)
991
994
  return [result]
992
-
995
+
993
996
  # Safety confirmation for status changes
994
997
  if not self.confirm_operation(context, access_key_id, f"update access key status to {status}"):
995
998
  result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
996
999
  return [result]
997
-
1000
+
998
1001
  response = self.execute_aws_call(
999
- iam_client,
1000
- "update_access_key",
1001
- UserName=username,
1002
- AccessKeyId=access_key_id,
1003
- Status=status
1002
+ iam_client, "update_access_key", UserName=username, AccessKeyId=access_key_id, Status=status
1004
1003
  )
1005
-
1004
+
1006
1005
  status_color = "green" if status == "Active" else "yellow"
1007
1006
  console.print(f"[{status_color}]✅ Access key {access_key_id} status updated to {status}[/{status_color}]")
1008
-
1007
+
1009
1008
  result.response_data = {
1010
1009
  "username": username,
1011
1010
  "access_key_id": access_key_id,
1012
1011
  "status": status,
1013
- "updated_at": datetime.now().isoformat()
1012
+ "updated_at": datetime.now().isoformat(),
1014
1013
  }
1015
1014
  result.mark_completed(OperationStatus.SUCCESS)
1016
1015
  logger.info(f"Successfully updated access key {access_key_id} status to {status}")
1017
-
1016
+
1018
1017
  except ClientError as e:
1019
1018
  error_msg = f"Failed to update access key status: {e}"
1020
1019
  logger.error(error_msg)
1021
1020
  result.mark_completed(OperationStatus.FAILED, error_msg)
1022
1021
  console.print(f"[red]❌ {error_msg}[/red]")
1023
-
1022
+
1024
1023
  return [result]
1025
1024
 
1026
1025
  def delete_access_key(self, context: OperationContext, username: str, access_key_id: str) -> List[OperationResult]:
1027
1026
  """
1028
1027
  Delete access key for specified user.
1029
-
1028
+
1030
1029
  Migrated from unSkript aws_delete_access_key function.
1031
-
1030
+
1032
1031
  Args:
1033
1032
  context: Operation context
1034
1033
  username: IAM username
1035
1034
  access_key_id: Access key ID to delete
1036
-
1035
+
1037
1036
  Returns:
1038
1037
  List of operation results
1039
1038
  """
1040
1039
  iam_client = self.get_client("iam")
1041
-
1040
+
1042
1041
  result = self.create_operation_result(
1043
1042
  context, "delete_access_key", "iam:access-key", f"{username}:{access_key_id}"
1044
1043
  )
1045
-
1044
+
1046
1045
  try:
1047
1046
  if context.dry_run:
1048
1047
  console.print(f"[yellow][DRY-RUN] Would delete access key {access_key_id} for user {username}[/yellow]")
1049
1048
  result.mark_completed(OperationStatus.DRY_RUN)
1050
1049
  return [result]
1051
-
1050
+
1052
1051
  # Strong confirmation required for deletion
1053
1052
  console.print(f"[red]⚠️ WARNING: This will permanently delete access key {access_key_id}[/red]")
1054
1053
  if not self.confirm_operation(context, access_key_id, f"PERMANENTLY DELETE access key"):
1055
1054
  result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
1056
1055
  return [result]
1057
-
1056
+
1058
1057
  response = self.execute_aws_call(
1059
- iam_client,
1060
- "delete_access_key",
1061
- UserName=username,
1062
- AccessKeyId=access_key_id
1058
+ iam_client, "delete_access_key", UserName=username, AccessKeyId=access_key_id
1063
1059
  )
1064
-
1060
+
1065
1061
  console.print(f"[green]✅ Access key {access_key_id} successfully deleted[/green]")
1066
-
1062
+
1067
1063
  result.response_data = {
1068
1064
  "username": username,
1069
1065
  "access_key_id": access_key_id,
1070
- "deleted_at": datetime.now().isoformat()
1066
+ "deleted_at": datetime.now().isoformat(),
1071
1067
  }
1072
1068
  result.mark_completed(OperationStatus.SUCCESS)
1073
1069
  logger.info(f"Successfully deleted access key {access_key_id} for user {username}")
1074
-
1070
+
1075
1071
  except ClientError as e:
1076
1072
  error_msg = f"Failed to delete access key: {e}"
1077
1073
  logger.error(error_msg)
1078
1074
  result.mark_completed(OperationStatus.FAILED, error_msg)
1079
1075
  console.print(f"[red]❌ {error_msg}[/red]")
1080
-
1076
+
1081
1077
  return [result]
1082
1078
 
1083
1079
  def rotate_access_keys(
@@ -1085,39 +1081,39 @@ class IAMOperations(BaseOperation):
1085
1081
  ) -> List[OperationResult]:
1086
1082
  """
1087
1083
  Complete access key rotation workflow combining all steps.
1088
-
1084
+
1089
1085
  This orchestrates the full unSkript notebook workflow:
1090
1086
  1. List expiring access keys
1091
- 2. Create new access keys
1087
+ 2. Create new access keys
1092
1088
  3. Deactivate old access keys
1093
1089
  4. Delete old access keys (optional)
1094
-
1090
+
1095
1091
  Args:
1096
1092
  context: Operation context
1097
1093
  threshold_days: Age threshold for rotation
1098
1094
  auto_rotate: If True, automatically rotates without confirmation per key
1099
-
1095
+
1100
1096
  Returns:
1101
1097
  List of operation results
1102
1098
  """
1103
1099
  results = []
1104
-
1100
+
1105
1101
  # Step 1: Find expiring access keys
1106
1102
  console.print(Panel("[blue]Step 1: Finding expiring access keys...[/blue]", title="Access Key Rotation"))
1107
1103
  expiring_result = self.list_expiring_access_keys(context, threshold_days=threshold_days)
1108
1104
  results.extend(expiring_result)
1109
-
1105
+
1110
1106
  if not expiring_result or expiring_result[0].status == OperationStatus.FAILED:
1111
1107
  return results
1112
-
1108
+
1113
1109
  expiring_keys_data = expiring_result[0].response_data.get("expiring_keys", [])
1114
-
1110
+
1115
1111
  if not expiring_keys_data:
1116
1112
  console.print(Panel("[green]✅ No access keys need rotation[/green]", title="Complete"))
1117
1113
  return results
1118
-
1114
+
1119
1115
  console.print(f"[yellow]Found {len(expiring_keys_data)} keys to rotate[/yellow]")
1120
-
1116
+
1121
1117
  if not auto_rotate:
1122
1118
  if not self.confirm_operation(context, f"{len(expiring_keys_data)} access keys", "rotate"):
1123
1119
  cancelled_result = self.create_operation_result(
@@ -1126,19 +1122,19 @@ class IAMOperations(BaseOperation):
1126
1122
  cancelled_result.mark_completed(OperationStatus.CANCELLED, "Rotation cancelled by user")
1127
1123
  results.append(cancelled_result)
1128
1124
  return results
1129
-
1125
+
1130
1126
  # Steps 2-4: Rotate each expiring key
1131
1127
  for key_data in expiring_keys_data:
1132
1128
  username = key_data["username"]
1133
1129
  old_access_key_id = key_data["access_key_id"]
1134
-
1130
+
1135
1131
  console.print(f"[cyan]Rotating access key for user: {username}[/cyan]")
1136
-
1132
+
1137
1133
  # Step 2: Create new access key
1138
1134
  console.print(f"[blue] → Creating new access key...[/blue]")
1139
1135
  create_result = self.create_access_key(context, username=username)
1140
1136
  results.extend(create_result)
1141
-
1137
+
1142
1138
  if create_result[0].status == OperationStatus.SUCCESS:
1143
1139
  # Step 3: Deactivate old access key
1144
1140
  console.print(f"[yellow] → Deactivating old access key...[/yellow]")
@@ -1146,19 +1142,17 @@ class IAMOperations(BaseOperation):
1146
1142
  context, username=username, access_key_id=old_access_key_id, status="Inactive"
1147
1143
  )
1148
1144
  results.extend(deactivate_result)
1149
-
1145
+
1150
1146
  if deactivate_result[0].status == OperationStatus.SUCCESS:
1151
1147
  # Step 4: Option to delete old key (with confirmation)
1152
1148
  console.print(f"[red] → Old key deactivated. Delete permanently?[/red]")
1153
- if auto_rotate or self.confirm_operation(
1154
- context, old_access_key_id, "delete old access key"
1155
- ):
1149
+ if auto_rotate or self.confirm_operation(context, old_access_key_id, "delete old access key"):
1156
1150
  delete_result = self.delete_access_key(
1157
1151
  context, username=username, access_key_id=old_access_key_id
1158
1152
  )
1159
1153
  results.extend(delete_result)
1160
1154
  else:
1161
1155
  console.print(f"[yellow] → Old key kept inactive for manual cleanup[/yellow]")
1162
-
1156
+
1163
1157
  console.print(Panel("[green]✅ Access key rotation workflow complete[/green]", title="Complete"))
1164
1158
  return results