runbooks 1.1.9__py3-none-any.whl → 1.1.10__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 (107) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/__init___optimized.py +2 -1
  3. runbooks/_platform/__init__.py +1 -1
  4. runbooks/cfat/cli.py +4 -3
  5. runbooks/cfat/cloud_foundations_assessment.py +1 -2
  6. runbooks/cfat/tests/test_cli.py +4 -1
  7. runbooks/cli/commands/finops.py +68 -19
  8. runbooks/cli/commands/inventory.py +796 -7
  9. runbooks/cli/commands/operate.py +65 -4
  10. runbooks/cloudops/cost_optimizer.py +1 -3
  11. runbooks/common/cli_decorators.py +6 -4
  12. runbooks/common/config_loader.py +787 -0
  13. runbooks/common/config_schema.py +280 -0
  14. runbooks/common/dry_run_framework.py +14 -2
  15. runbooks/common/mcp_integration.py +238 -0
  16. runbooks/finops/ebs_cost_optimizer.py +7 -4
  17. runbooks/finops/elastic_ip_optimizer.py +7 -4
  18. runbooks/finops/infrastructure/__init__.py +3 -2
  19. runbooks/finops/infrastructure/commands.py +7 -4
  20. runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
  21. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
  22. runbooks/finops/nat_gateway_optimizer.py +7 -4
  23. runbooks/finops/tests/run_tests.py +1 -1
  24. runbooks/inventory/ArgumentsClass.py +2 -1
  25. runbooks/inventory/README.md +111 -12
  26. runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
  27. runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
  28. runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
  29. runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
  30. runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
  31. runbooks/inventory/account_class.py +0 -1
  32. runbooks/inventory/all_my_instances_wrapper.py +4 -8
  33. runbooks/inventory/aws_organization.png +0 -0
  34. runbooks/inventory/check_cloudtrail_compliance.py +4 -4
  35. runbooks/inventory/check_controltower_readiness.py +50 -47
  36. runbooks/inventory/check_landingzone_readiness.py +35 -31
  37. runbooks/inventory/cloud_foundations_integration.py +8 -3
  38. runbooks/inventory/core/collector.py +201 -1
  39. runbooks/inventory/discovery.md +2 -1
  40. runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
  41. runbooks/inventory/drift_detection_cli.py +8 -68
  42. runbooks/inventory/find_cfn_drift_detection.py +14 -4
  43. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
  44. runbooks/inventory/find_cfn_stackset_drift.py +5 -5
  45. runbooks/inventory/find_ec2_security_groups.py +6 -3
  46. runbooks/inventory/find_landingzone_versions.py +5 -5
  47. runbooks/inventory/find_vpc_flow_logs.py +5 -5
  48. runbooks/inventory/inventory.sh +20 -7
  49. runbooks/inventory/inventory_mcp_cli.py +4 -0
  50. runbooks/inventory/inventory_modules.py +9 -7
  51. runbooks/inventory/list_cfn_stacks.py +18 -8
  52. runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
  53. runbooks/inventory/list_cfn_stackset_operations.py +32 -20
  54. runbooks/inventory/list_cfn_stacksets.py +7 -4
  55. runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
  56. runbooks/inventory/list_ds_directories.py +3 -3
  57. runbooks/inventory/list_ec2_availability_zones.py +7 -3
  58. runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
  59. runbooks/inventory/list_ec2_instances.py +1 -1
  60. runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
  61. runbooks/inventory/list_elbs_load_balancers.py +7 -3
  62. runbooks/inventory/list_enis_network_interfaces.py +3 -3
  63. runbooks/inventory/list_guardduty_detectors.py +9 -5
  64. runbooks/inventory/list_iam_policies.py +7 -3
  65. runbooks/inventory/list_iam_roles.py +3 -3
  66. runbooks/inventory/list_iam_saml_providers.py +8 -4
  67. runbooks/inventory/list_lambda_functions.py +8 -4
  68. runbooks/inventory/list_org_accounts.py +306 -276
  69. runbooks/inventory/list_org_accounts_users.py +45 -9
  70. runbooks/inventory/list_rds_db_instances.py +4 -4
  71. runbooks/inventory/list_route53_hosted_zones.py +3 -3
  72. runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
  73. runbooks/inventory/list_sns_topics.py +4 -4
  74. runbooks/inventory/list_ssm_parameters.py +6 -3
  75. runbooks/inventory/list_vpc_subnets.py +8 -4
  76. runbooks/inventory/list_vpcs.py +15 -4
  77. runbooks/inventory/mcp_vpc_validator.py +6 -0
  78. runbooks/inventory/organizations_discovery.py +17 -3
  79. runbooks/inventory/organizations_utils.py +553 -0
  80. runbooks/inventory/output_formatters.py +422 -0
  81. runbooks/inventory/recover_cfn_stack_ids.py +5 -5
  82. runbooks/inventory/run_on_multi_accounts.py +3 -3
  83. runbooks/inventory/tag_coverage.py +481 -0
  84. runbooks/inventory/validation_utils.py +358 -0
  85. runbooks/inventory/verify_ec2_security_groups.py +18 -5
  86. runbooks/inventory/vpc_architecture_validator.py +7 -1
  87. runbooks/inventory/vpc_dependency_analyzer.py +6 -0
  88. runbooks/main_final.py +2 -2
  89. runbooks/main_ultra_minimal.py +2 -2
  90. runbooks/mcp/integration.py +6 -4
  91. runbooks/remediation/acm_remediation.py +2 -2
  92. runbooks/remediation/cloudtrail_remediation.py +2 -2
  93. runbooks/remediation/cognito_remediation.py +2 -2
  94. runbooks/remediation/dynamodb_remediation.py +2 -2
  95. runbooks/remediation/ec2_remediation.py +2 -2
  96. runbooks/remediation/kms_remediation.py +2 -2
  97. runbooks/remediation/lambda_remediation.py +2 -2
  98. runbooks/remediation/rds_remediation.py +2 -2
  99. runbooks/remediation/s3_remediation.py +1 -1
  100. runbooks/vpc/cloudtrail_audit_integration.py +1 -1
  101. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
  102. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/RECORD +106 -100
  103. runbooks/__init__.py.backup +0 -134
  104. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
  105. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
  106. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
  107. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/top_level.txt +0 -0
@@ -288,6 +288,150 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
288
288
  """Get current AWS account ID."""
289
289
  return self.get_account_id()
290
290
 
291
+ def _display_inventory_summary(self, results: Dict[str, Any]) -> None:
292
+ """
293
+ Display actionable inventory summary with business value.
294
+
295
+ Transforms technical data collection into executive-ready business intelligence.
296
+ Shows: Resource counts, cost estimates, security findings, actionable recommendations.
297
+ """
298
+ from runbooks.common.rich_utils import console, create_table, print_header, print_info
299
+
300
+ # Skip display if no resources collected
301
+ if not results.get("resources"):
302
+ print_info("No resources discovered (check AWS permissions)")
303
+ return
304
+
305
+ # Create summary table
306
+ table = create_table(
307
+ title="📊 AWS Resource Inventory Summary",
308
+ columns=[
309
+ {"header": "Resource Type", "style": "cyan"},
310
+ {"header": "Count", "style": "green", "justify": "right"},
311
+ {"header": "Key Findings", "style": "yellow"}
312
+ ]
313
+ )
314
+
315
+ total_resources = 0
316
+ findings_summary = []
317
+
318
+ # Process each resource type
319
+ for resource_type, data in results.get("resources", {}).items():
320
+ if not data:
321
+ continue
322
+
323
+ count = len(data) if isinstance(data, list) else data.get("count", 0)
324
+ total_resources += count
325
+
326
+ # Generate findings for this resource type
327
+ findings = self._generate_resource_findings(resource_type, data)
328
+ findings_text = findings if findings else "✅ No issues"
329
+
330
+ # Add to table
331
+ table.add_row(
332
+ resource_type.upper(),
333
+ str(count),
334
+ findings_text
335
+ )
336
+
337
+ # Collect findings for recommendations
338
+ if findings and findings != "✅ No issues":
339
+ findings_summary.append({
340
+ "resource_type": resource_type,
341
+ "finding": findings,
342
+ "data": data
343
+ })
344
+
345
+ # Display table
346
+ console.print("\n")
347
+ console.print(table)
348
+
349
+ # Display summary metrics
350
+ account_id = results.get("metadata", {}).get("account_ids", ["Unknown"])[0] if results.get("metadata", {}).get("account_ids") else "Unknown"
351
+ console.print(f"\n📋 Total Resources: [bold]{total_resources}[/bold] across [bold]{len(results.get('resources', {}))}[/bold] services")
352
+ console.print(f"🏢 Account: [cyan]{account_id}[/cyan]")
353
+
354
+ # Display actionable recommendations
355
+ if findings_summary:
356
+ console.print("\n💡 [bold]Actionable Recommendations:[/bold]")
357
+ recommendations = self._generate_actionable_recommendations(findings_summary)
358
+ for i, rec in enumerate(recommendations[:5], 1): # Top 5
359
+ console.print(f" {i}. {rec}")
360
+ else:
361
+ console.print("\n✅ [green]No immediate action items identified[/green]")
362
+
363
+ console.print("") # Blank line for readability
364
+
365
+ def _generate_resource_findings(self, resource_type: str, data: Any) -> str:
366
+ """
367
+ Generate business-focused findings for a resource type.
368
+
369
+ Returns human-readable finding (e.g., "12 stopped instances")
370
+ NOT technical data (e.g., "state=stopped count=12")
371
+ """
372
+ if not data:
373
+ return "✅ No issues"
374
+
375
+ findings = []
376
+
377
+ # EC2-specific findings
378
+ if resource_type == "ec2":
379
+ if isinstance(data, list):
380
+ stopped = sum(1 for instance in data if instance.get("State", {}).get("Name") == "stopped")
381
+ if stopped > 0:
382
+ findings.append(f"{stopped} stopped (cost waste)")
383
+
384
+ no_tags = sum(1 for instance in data if not instance.get("Tags"))
385
+ if no_tags > 0:
386
+ findings.append(f"{no_tags} untagged (compliance)")
387
+
388
+ # S3-specific findings
389
+ elif resource_type == "s3":
390
+ if isinstance(data, list):
391
+ # Note: Would need actual encryption status from API
392
+ # For now, placeholder for demonstration
393
+ findings.append("Review encryption status")
394
+
395
+ # RDS-specific findings
396
+ elif resource_type == "rds":
397
+ if isinstance(data, list):
398
+ # Placeholder for backup status
399
+ findings.append("Verify backup configuration")
400
+
401
+ return " | ".join(findings) if findings else "✅ No issues"
402
+
403
+ def _generate_actionable_recommendations(self, findings_summary: List[Dict]) -> List[str]:
404
+ """
405
+ Generate specific, actionable recommendations with commands to run.
406
+
407
+ Format: "Action → Business Value (Command to execute)"
408
+ """
409
+ recommendations = []
410
+
411
+ for finding in findings_summary:
412
+ resource_type = finding["resource_type"]
413
+
414
+ if resource_type == "ec2":
415
+ if "stopped" in finding["finding"]:
416
+ recommendations.append(
417
+ "[yellow]Terminate stopped EC2 instances[/yellow] → "
418
+ "Reduce compute costs (Review with: [cyan]runbooks operate ec2 list --status stopped[/cyan])"
419
+ )
420
+
421
+ elif resource_type == "s3":
422
+ recommendations.append(
423
+ "[yellow]Review S3 bucket security[/yellow] → "
424
+ "Ensure compliance (Check with: [cyan]runbooks security s3-audit[/cyan])"
425
+ )
426
+
427
+ elif resource_type == "rds":
428
+ recommendations.append(
429
+ "[yellow]Verify RDS backup configuration[/yellow] → "
430
+ "Prevent data loss (Check with: [cyan]runbooks operate rds list-backups[/cyan])"
431
+ )
432
+
433
+ return recommendations
434
+
291
435
  def collect_inventory(
292
436
  self, resource_types: List[str], account_ids: List[str], include_costs: bool = False,
293
437
  resource_filters: Optional[Dict[str, Any]] = None
@@ -440,6 +584,16 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
440
584
  else:
441
585
  logger.info(f"Inventory collection completed in {duration:.1f}s")
442
586
 
587
+ # Display business value summary to user (unless in short mode)
588
+ # Check for short mode flag in resource_filters
589
+ short_mode = resource_filters.get("short", False) if resource_filters else False
590
+ if not short_mode:
591
+ try:
592
+ self._display_inventory_summary(results)
593
+ except Exception as display_error:
594
+ # Graceful degradation if display fails - don't break core functionality
595
+ logger.warning(f"Failed to display inventory summary: {display_error}")
596
+
443
597
  return results
444
598
 
445
599
  except Exception as e:
@@ -2876,7 +3030,44 @@ def run_inventory_collection(**kwargs) -> Dict[str, Any]:
2876
3030
  account_ids = [collector.get_current_account_id()]
2877
3031
  if use_all_profiles:
2878
3032
  try:
2879
- account_ids = collector.get_organization_accounts()
3033
+ # PHASE 3: Enhanced Organizations discovery using proven Phase 2 pattern
3034
+ # Import Organizations discovery functions (DRY reuse from inventory_modules.py)
3035
+ from runbooks.inventory.inventory_modules import get_org_accounts_from_profiles, get_profiles
3036
+
3037
+ console.print("[cyan]🏢 Discovering AWS Organization accounts via Organizations API...[/cyan]")
3038
+
3039
+ # Use management profile for Organizations API access (same as Phase 2)
3040
+ profile_list = get_profiles(fprofiles=[profile] if profile else None)
3041
+ console.print(f"[dim]Querying Organizations API with profile: {profile or 'default'}[/dim]")
3042
+
3043
+ # Get organization accounts using proven FinOps pattern
3044
+ org_accounts = get_org_accounts_from_profiles(profile_list)
3045
+
3046
+ # Extract account IDs from organization accounts (Phase 2 proven pattern)
3047
+ discovered_account_ids = []
3048
+ for acct in org_accounts:
3049
+ if acct.get("Success") and acct.get("RootAcct") and acct.get("aws_acct"):
3050
+ # Management account
3051
+ discovered_account_ids.append(acct["aws_acct"].acct_number)
3052
+
3053
+ # Child accounts in organization
3054
+ for child in acct["aws_acct"].ChildAccounts:
3055
+ discovered_account_ids.append(child["AccountId"])
3056
+
3057
+ if discovered_account_ids:
3058
+ account_ids = discovered_account_ids
3059
+ console.print(
3060
+ f"[green]✅ Discovered {len(account_ids)} organization accounts[/green]"
3061
+ )
3062
+ console.print(
3063
+ f"[cyan]📊 Analysis Scope: Organization-wide with Landing Zone support[/cyan]\n"
3064
+ )
3065
+ logger.info(f"Organizations discovery successful: {len(account_ids)} accounts")
3066
+ else:
3067
+ console.print(
3068
+ f"[yellow]⚠️ Organizations discovery returned no accounts, using current account[/yellow]"
3069
+ )
3070
+ logger.warning("Organizations discovery yielded no accounts")
2880
3071
 
2881
3072
  # Apply skip_profiles filtering (v1.1.9 - Group 1: Resource Filtering)
2882
3073
  if skip_profiles:
@@ -2886,8 +3077,17 @@ def run_inventory_collection(**kwargs) -> Dict[str, Any]:
2886
3077
  logger.info(f"Profile exclusion filter active: {len(skip_profiles)} profiles to skip")
2887
3078
  # Implementation note: Profile filtering requires profile-to-account mapping
2888
3079
  # which is typically handled at the CLI layer before collector initialization
3080
+
2889
3081
  except Exception as e:
3082
+ # Graceful fallback to single account on Organizations discovery failure
3083
+ console.print(
3084
+ f"[yellow]⚠️ Organizations discovery error: {e}[/yellow]"
3085
+ )
3086
+ console.print(
3087
+ f"[dim]Falling back to single account mode[/dim]\n"
3088
+ )
2890
3089
  logger.warning(f"Failed to get organization accounts: {e}")
3090
+ account_ids = [collector.get_current_account_id()]
2891
3091
 
2892
3092
  # Collect inventory with resource filters (v1.1.8)
2893
3093
  try:
@@ -21,7 +21,8 @@ Based on real testing with enterprise AWS profiles, the CloudOps-Runbooks invent
21
21
 
22
22
  ```bash
23
23
  # Single resource type (TESTED ✅)
24
- runbooks inventory collect --resources ec2 --dry-run
24
+
25
+
25
26
 
26
27
  # Multiple resources (TESTED ✅)
27
28
  runbooks inventory collect --resources ec2,rds,s3,lambda --dry-run
@@ -57,9 +57,10 @@ from time import time
57
57
  from typing import Any, Dict, List, Optional
58
58
 
59
59
  import boto3
60
- from ArgumentsClass import CommonArguments
60
+ from runbooks.inventory.ArgumentsClass import CommonArguments
61
61
  from runbooks.common.rich_utils import console
62
62
  from graphviz import Digraph
63
+ from runbooks import __version__
63
64
 
64
65
  # Optional imports for enhanced features
65
66
  try:
@@ -72,7 +73,6 @@ except ImportError:
72
73
  JUPYTER_AVAILABLE = False
73
74
  logging.debug("Jupyter widgets not available - interactive features disabled")
74
75
 
75
- __version__ = "2025.04.09"
76
76
 
77
77
 
78
78
  # Visual styling constants
@@ -96,6 +96,10 @@ aws_policy_type_list = [
96
96
  "DECLARATIVE_POLICY_EC2",
97
97
  ]
98
98
 
99
+ # Skip filters (set by Modern CLI wrapper or can be empty)
100
+ excluded_accounts: set = set()
101
+ excluded_ous: set = set()
102
+
99
103
  #####################
100
104
  # Function Definitions
101
105
  #####################
@@ -142,7 +146,7 @@ def parse_args(f_args):
142
146
  help="Use this parameter to specify where to start from (Defaults to the root)",
143
147
  )
144
148
  local.add_argument(
145
- "--format",
149
+ "--output-format",
146
150
  dest="output_format",
147
151
  choices=["graphviz", "mermaid", "diagrams"],
148
152
  default="graphviz",
@@ -416,22 +420,49 @@ def generate_diagrams(org_structure: Any, filename: str) -> None:
416
420
 
417
421
  def build_diagram(node: Dict[str, Any]):
418
422
  """
419
- Recursively build diagram nodes from the org structure.
420
- For nodes with children, creates a Cluster.
423
+ Recursively build diagram nodes from the org structure with enhanced readability.
424
+
425
+ Enhancements:
426
+ - Account counts displayed per OU (matching graphviz UX pattern)
427
+ - Node IDs included for traceability
428
+ - Multi-line labels with escaped newlines
421
429
  """
422
430
  name = node.get("name", node.get("id", "Unnamed"))
431
+ node_id = node.get("id", "unknown")
432
+
423
433
  if "children" in node and node["children"]:
424
- with Cluster(name):
434
+ # Count accounts directly under this OU (match graphviz UX)
435
+ account_count = sum(1 for child in node["children"] if not child.get("children"))
436
+
437
+ # Enhanced cluster label with account count (graphviz pattern)
438
+ cluster_label = f"{name}\\n({account_count} accounts)"
439
+
440
+ with Cluster(cluster_label):
425
441
  children_nodes = [build_diagram(child) for child in node["children"]]
426
- current = OrganizationsOrganizationalUnit(name)
442
+
443
+ # Use simplified OU representation with ID
444
+ current = OrganizationsOrganizationalUnit(f"{name}\\n{node_id}")
427
445
  for child in children_nodes:
428
446
  current >> child
429
447
  return current
430
448
  else:
431
- return OrganizationsAccount(name)
449
+ # Account leaf nodes with clear IDs
450
+ account_label = f"{name}\\n{node_id}"
451
+ return OrganizationsAccount(account_label)
432
452
 
433
453
  try:
434
- with Diagram("AWS Organization Diagram", filename=filename, show=False, direction="LR"):
454
+ with Diagram(
455
+ "AWS Organization Structure",
456
+ filename=filename,
457
+ show=False,
458
+ direction="TB", # Top-bottom (traditional org chart layout)
459
+ graph_attr={
460
+ "splines": "ortho", # Orthogonal edge routing (cleaner lines)
461
+ "nodesep": "1.5", # Horizontal spacing between nodes
462
+ "ranksep": "2.0", # Vertical spacing between ranks
463
+ "concentrate": "true", # Merge edges where appropriate
464
+ },
465
+ ):
435
466
  build_diagram(org_structure)
436
467
  print(f"Diagrams image successfully generated as '[red]{filename}'")
437
468
  logging.info(f"Diagrams image successfully generated as {filename}")
@@ -497,12 +528,21 @@ def draw_org(froot: str, filename: str):
497
528
  Description: Recursively traverse the OUs and accounts and update the diagram
498
529
  @param ou_id: The ID of the OU to start from
499
530
  """
531
+ # Check if this OU should be excluded
532
+ if ou_id in excluded_ous:
533
+ logging.info(f"Skipping excluded OU: {ou_id}")
534
+ return
535
+
500
536
  # Retrieve the details of the current OU
501
537
  if ou_id[0] == "r":
502
538
  ou_name = "Root"
503
539
  else:
504
540
  ou = org_client.describe_organizational_unit(OrganizationalUnitId=ou_id)
505
541
  ou_name = ou["OrganizationalUnit"]["Name"]
542
+ # Also check if OU name is in exclusion list
543
+ if ou_name in excluded_ous:
544
+ logging.info(f"Skipping excluded OU by name: {ou_name} ({ou_id})")
545
+ return
506
546
 
507
547
  if pPolicy:
508
548
  # Retrieve the policies associated with this OU
@@ -543,6 +583,12 @@ def draw_org(froot: str, filename: str):
543
583
  for account in all_accounts:
544
584
  account_id = account["Id"]
545
585
  account_name = account["Name"]
586
+
587
+ # Skip excluded accounts
588
+ if account_id in excluded_accounts:
589
+ logging.info(f"Skipping excluded account: {account_name} ({account_id})")
590
+ continue
591
+
546
592
  # Add the account as a node in the diagram
547
593
  if account["Status"] == "SUSPENDED":
548
594
  dot.node(
@@ -32,20 +32,20 @@ from typing import List, Optional
32
32
  from ..common.profile_utils import get_profile_for_operation, validate_profile_access
33
33
  from ..common.rich_utils import console, print_error, print_info, print_success
34
34
  from .mcp_inventory_validator import (
35
+
36
+
37
+ # Terminal control constants
35
38
  create_inventory_mcp_validator,
36
39
  generate_drift_report,
37
40
  validate_inventory_results_with_mcp,
38
41
  )
39
42
 
40
43
 
41
- @click.group()
42
- def drift():
43
- """Infrastructure drift detection and validation commands."""
44
- pass
45
44
 
46
-
47
- @drift.command("detect")
48
- @click.option("--profile", help="AWS profile to use (overrides environment variables)")
45
+ # Terminal control constants
46
+ ERASE_LINE = '\x1b[2K'
47
+ @click.command()
48
+ @click.option("--profile", "-p", help="AWS profile to use (overrides environment variables)")
49
49
  @click.option("--profiles", help="Comma-separated list of AWS profiles for multi-account analysis")
50
50
  @click.option(
51
51
  "--terraform-dir",
@@ -60,7 +60,7 @@ def drift():
60
60
  )
61
61
  @click.option("--output-file", help="File path to save drift report (optional)")
62
62
  @click.option("--threshold", type=float, default=99.5, help="Accuracy threshold for drift detection (default: 99.5%)")
63
- def detect_drift(
63
+ def drift(
64
64
  profile: Optional[str],
65
65
  profiles: Optional[str],
66
66
  terraform_dir: str,
@@ -152,66 +152,6 @@ def detect_drift(
152
152
  raise click.Abort()
153
153
 
154
154
 
155
- @drift.command("report")
156
- @click.option("--profile", help="AWS profile to use for single-account analysis")
157
- @click.option(
158
- "--terraform-dir",
159
- default="/Volumes/Working/1xOps/CloudOps-Runbooks/terraform-aws",
160
- help="Path to terraform configuration directory",
161
- )
162
- @click.option(
163
- "--format",
164
- "report_format",
165
- type=click.Choice(["json", "csv", "markdown"]),
166
- default="json",
167
- help="Report output format",
168
- )
169
- @click.option("--output", "output_file", required=True, help="Output file path for drift report")
170
- def generate_report(profile: Optional[str], terraform_dir: str, report_format: str, output_file: str):
171
- """
172
- Generate comprehensive infrastructure drift report.
173
-
174
- Creates detailed drift analysis report with actionable recommendations
175
- for infrastructure as code management and compliance.
176
- """
177
- try:
178
- # Use default profile if none specified
179
- if not profile:
180
- profile = get_profile_for_operation("operational", None)
181
-
182
- print_info(f"Generating drift report for profile: {profile}")
183
-
184
- # Validate profile
185
- if not validate_profile_access(profile, "drift-reporting"):
186
- print_error(f"Profile '{profile}' validation failed")
187
- return
188
-
189
- # Create mock inventory for demonstration
190
- mock_inventory = _create_mock_inventory_data([profile])
191
-
192
- # Generate comprehensive drift report
193
- drift_report = generate_drift_report([profile], mock_inventory, terraform_dir)
194
-
195
- # Export report
196
- _export_drift_report(drift_report, output_file, report_format)
197
-
198
- print_success(f"Drift report generated: {output_file}")
199
-
200
- # Display summary
201
- accounts_analyzed = drift_report.get("accounts_analyzed", 0)
202
- overall_accuracy = drift_report.get("overall_accuracy", 0)
203
- drift_detected = drift_report.get("drift_detected", False)
204
-
205
- console.print(f"[dim]📊 Analysis Summary:[/]")
206
- console.print(f"[dim] Accounts analyzed: {accounts_analyzed}[/]")
207
- console.print(f"[dim] Overall accuracy: {overall_accuracy:.1f}%[/]")
208
- console.print(f"[dim] Drift detected: {'Yes' if drift_detected else 'No'}[/]")
209
-
210
- except Exception as e:
211
- print_error(f"Report generation failed: {str(e)}")
212
- raise click.Abort()
213
-
214
-
215
155
  def _create_mock_inventory_data(profiles: List[str]) -> dict:
216
156
  """Create mock inventory data for demonstration purposes."""
217
157
  mock_data = {}
@@ -66,13 +66,13 @@ Future Enhancements:
66
66
 
67
67
  import logging
68
68
 
69
- import Inventory_Modules
70
- from account_class import aws_acct_access
71
- from ArgumentsClass import CommonArguments
69
+ from runbooks.inventory import inventory_modules as Inventory_Modules
70
+ from runbooks.inventory.account_class import aws_acct_access
71
+ from runbooks.inventory.ArgumentsClass import CommonArguments
72
72
  from botocore.exceptions import ClientError
73
73
  from runbooks.common.rich_utils import console
74
+ from runbooks import __version__
74
75
 
75
- __version__ = "2023.05.04"
76
76
 
77
77
  # Configure comprehensive argument parsing for CloudFormation drift detection operations
78
78
  parser = CommonArguments()
@@ -174,6 +174,16 @@ if aws_acct.AccountType == "Root":
174
174
  "AccountStatus": aws_acct.AccountStatus, # Account operational status
175
175
  }
176
176
  ]
177
+ else:
178
+ # Non-root account: Initialize ChildAccounts with single account entry
179
+ ChildAccounts = [
180
+ {
181
+ "MgmtAccount": aws_acct.acct_number, # Management account identifier
182
+ "AccountId": aws_acct.acct_number, # Account number for single account scope
183
+ "AccountEmail": aws_acct.MgmtEmail if hasattr(aws_acct, 'MgmtEmail') else "N/A", # Management email
184
+ "AccountStatus": aws_acct.AccountStatus if hasattr(aws_acct, 'AccountStatus') else "ACTIVE", # Account status
185
+ }
186
+ ]
177
187
 
178
188
  # Initialize drift detection operation tracking and regional scope configuration
179
189
  NumStacksFound = 0 # Counter for tracking total stacks processed for drift detection
@@ -71,13 +71,13 @@ import sys
71
71
  from os.path import split
72
72
  from time import time
73
73
 
74
- from account_class import aws_acct_access
75
- from ArgumentsClass import CommonArguments
74
+ from runbooks.inventory.account_class import aws_acct_access
75
+ from runbooks.inventory.ArgumentsClass import CommonArguments
76
76
 
77
77
  # import simplejson as json
78
78
  from botocore.exceptions import ClientError
79
79
  from runbooks.common.rich_utils import console
80
- from Inventory_Modules import (
80
+ from runbooks.inventory.inventory_modules import (
81
81
  display_results,
82
82
  find_stack_instances3,
83
83
  find_stacks2,
@@ -86,8 +86,11 @@ from Inventory_Modules import (
86
86
  get_regions3,
87
87
  print_timings,
88
88
  )
89
+ from runbooks import __version__
90
+
91
+ # Terminal control constants
92
+ ERASE_LINE = '\x1b[2K'
89
93
 
90
- __version__ = "2024.05.18"
91
94
  begin_time = time()
92
95
 
93
96
 
@@ -650,7 +653,6 @@ if __name__ == "__main__":
650
653
  logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
651
654
  logging.getLogger("urllib3").setLevel(logging.CRITICAL)
652
655
 
653
- ERASE_LINE = "\x1b[2K"
654
656
  begin_time = time()
655
657
 
656
658
  # Setup credentials and regions (filtered by what they wanted to check)
@@ -66,15 +66,15 @@ from datetime import datetime
66
66
  from os.path import split
67
67
  from time import time
68
68
 
69
- import Inventory_Modules
70
- from account_class import aws_acct_access
71
- from ArgumentsClass import CommonArguments
69
+ from runbooks.inventory import inventory_modules as Inventory_Modules
70
+ from runbooks.inventory.account_class import aws_acct_access
71
+ from runbooks.inventory.ArgumentsClass import CommonArguments
72
72
  from botocore.exceptions import ClientError
73
73
  from runbooks.common.rich_utils import console
74
- from Inventory_Modules import display_results
74
+ from runbooks.inventory.inventory_modules import display_results
75
+ from runbooks import __version__
75
76
 
76
77
  # Initialize colorama for cross-platform colored terminal output
77
- __version__ = "2024.03.06"
78
78
  begin_time = time() # Script execution timing for performance monitoring
79
79
  sleep_interval = 5 # Default wait interval for drift detection operations
80
80
 
@@ -60,19 +60,22 @@ from queue import Queue
60
60
  from threading import Thread
61
61
  from time import time
62
62
 
63
- from ArgumentsClass import CommonArguments
63
+ from runbooks.inventory.ArgumentsClass import CommonArguments
64
64
  from botocore.exceptions import ClientError
65
65
  from runbooks.common.rich_utils import console
66
- from Inventory_Modules import (
66
+ from runbooks.inventory.inventory_modules import (
67
67
  display_results,
68
68
  find_references_to_security_groups2,
69
69
  find_security_groups2,
70
70
  get_all_credentials,
71
71
  )
72
+ from runbooks import __version__
73
+
74
+ # Terminal control constants
75
+ ERASE_LINE = '\x1b[2K'
72
76
  # Migrated to Rich.Progress - see rich_utils.py for enterprise UX standards
73
77
  # from tqdm.auto import tqdm
74
78
 
75
- __version__ = "2024.09.24"
76
79
  begin_time = time()
77
80
 
78
81
 
@@ -73,14 +73,14 @@ License: MIT
73
73
  import logging
74
74
 
75
75
  import boto3
76
- import Inventory_Modules
77
- from ArgumentsClass import CommonArguments
76
+ from runbooks.inventory import inventory_modules as Inventory_Modules
77
+ from runbooks.inventory.ArgumentsClass import CommonArguments
78
78
  from botocore.exceptions import ClientError, CredentialRetrievalError, InvalidConfigError
79
- # colorama removed - migrated to Rich
79
+ from runbooks.common.rich_utils import console
80
+ from runbooks import __version__
80
81
 
81
- # Initialize colorama for cross-platform colored terminal output
82
+ # colorama removed - migrated to Rich
82
83
 
83
- __version__ = "2023.05.31"
84
84
 
85
85
  # Configure comprehensive CLI argument parsing for Landing Zone discovery operations
86
86
  parser = CommonArguments()
@@ -88,16 +88,16 @@ from time import sleep, time
88
88
  from typing import Any, List
89
89
 
90
90
  import boto3
91
- import Inventory_Modules
92
- from account_class import aws_acct_access
93
- from ArgumentsClass import CommonArguments
91
+ from runbooks.inventory import inventory_modules as Inventory_Modules
92
+ from runbooks.inventory.account_class import aws_acct_access
93
+ from runbooks.inventory.ArgumentsClass import CommonArguments
94
94
  from botocore.config import Config
95
95
  from botocore.exceptions import ClientError
96
96
  from runbooks.common.rich_utils import console
97
- from Inventory_Modules import RemoveCoreAccounts, display_results, get_all_credentials, get_regions3
97
+ from runbooks.inventory.inventory_modules import RemoveCoreAccounts, display_results, get_all_credentials, get_regions3
98
+ from runbooks import __version__
98
99
 
99
100
  # Initialize colorama for cross-platform colored terminal output
100
- __version__ = "2024.03.10"
101
101
  begin_time = time() # Script execution timing for performance monitoring
102
102
  sleep_interval = 5 # Default wait interval for CloudWatch Logs query processing
103
103