runbooks 1.1.4__py3-none-any.whl → 1.1.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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +138 -35
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +201 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +55 -51
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +732 -695
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.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(
|
411
|
-
|
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(
|
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(
|
827
|
-
|
827
|
+
paginator = iam_client.get_paginator("list_users")
|
828
|
+
|
828
829
|
for page in paginator.paginate():
|
829
|
-
for user in page[
|
830
|
-
username = user[
|
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[
|
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(
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
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 [
|
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
|