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.
- runbooks/__init__.py +1 -1
- runbooks/__init___optimized.py +2 -1
- runbooks/_platform/__init__.py +1 -1
- runbooks/cfat/cli.py +4 -3
- runbooks/cfat/cloud_foundations_assessment.py +1 -2
- runbooks/cfat/tests/test_cli.py +4 -1
- runbooks/cli/commands/finops.py +68 -19
- runbooks/cli/commands/inventory.py +796 -7
- runbooks/cli/commands/operate.py +65 -4
- runbooks/cloudops/cost_optimizer.py +1 -3
- runbooks/common/cli_decorators.py +6 -4
- runbooks/common/config_loader.py +787 -0
- runbooks/common/config_schema.py +280 -0
- runbooks/common/dry_run_framework.py +14 -2
- runbooks/common/mcp_integration.py +238 -0
- runbooks/finops/ebs_cost_optimizer.py +7 -4
- runbooks/finops/elastic_ip_optimizer.py +7 -4
- runbooks/finops/infrastructure/__init__.py +3 -2
- runbooks/finops/infrastructure/commands.py +7 -4
- runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
- runbooks/finops/nat_gateway_optimizer.py +7 -4
- runbooks/finops/tests/run_tests.py +1 -1
- runbooks/inventory/ArgumentsClass.py +2 -1
- runbooks/inventory/README.md +111 -12
- runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
- runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
- runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
- runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
- runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
- runbooks/inventory/account_class.py +0 -1
- runbooks/inventory/all_my_instances_wrapper.py +4 -8
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/check_cloudtrail_compliance.py +4 -4
- runbooks/inventory/check_controltower_readiness.py +50 -47
- runbooks/inventory/check_landingzone_readiness.py +35 -31
- runbooks/inventory/cloud_foundations_integration.py +8 -3
- runbooks/inventory/core/collector.py +201 -1
- runbooks/inventory/discovery.md +2 -1
- runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
- runbooks/inventory/drift_detection_cli.py +8 -68
- runbooks/inventory/find_cfn_drift_detection.py +14 -4
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
- runbooks/inventory/find_cfn_stackset_drift.py +5 -5
- runbooks/inventory/find_ec2_security_groups.py +6 -3
- runbooks/inventory/find_landingzone_versions.py +5 -5
- runbooks/inventory/find_vpc_flow_logs.py +5 -5
- runbooks/inventory/inventory.sh +20 -7
- runbooks/inventory/inventory_mcp_cli.py +4 -0
- runbooks/inventory/inventory_modules.py +9 -7
- runbooks/inventory/list_cfn_stacks.py +18 -8
- runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
- runbooks/inventory/list_cfn_stackset_operations.py +32 -20
- runbooks/inventory/list_cfn_stacksets.py +7 -4
- runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
- runbooks/inventory/list_ds_directories.py +3 -3
- runbooks/inventory/list_ec2_availability_zones.py +7 -3
- runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
- runbooks/inventory/list_ec2_instances.py +1 -1
- runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
- runbooks/inventory/list_elbs_load_balancers.py +7 -3
- runbooks/inventory/list_enis_network_interfaces.py +3 -3
- runbooks/inventory/list_guardduty_detectors.py +9 -5
- runbooks/inventory/list_iam_policies.py +7 -3
- runbooks/inventory/list_iam_roles.py +3 -3
- runbooks/inventory/list_iam_saml_providers.py +8 -4
- runbooks/inventory/list_lambda_functions.py +8 -4
- runbooks/inventory/list_org_accounts.py +306 -276
- runbooks/inventory/list_org_accounts_users.py +45 -9
- runbooks/inventory/list_rds_db_instances.py +4 -4
- runbooks/inventory/list_route53_hosted_zones.py +3 -3
- runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
- runbooks/inventory/list_sns_topics.py +4 -4
- runbooks/inventory/list_ssm_parameters.py +6 -3
- runbooks/inventory/list_vpc_subnets.py +8 -4
- runbooks/inventory/list_vpcs.py +15 -4
- runbooks/inventory/mcp_vpc_validator.py +6 -0
- runbooks/inventory/organizations_discovery.py +17 -3
- runbooks/inventory/organizations_utils.py +553 -0
- runbooks/inventory/output_formatters.py +422 -0
- runbooks/inventory/recover_cfn_stack_ids.py +5 -5
- runbooks/inventory/run_on_multi_accounts.py +3 -3
- runbooks/inventory/tag_coverage.py +481 -0
- runbooks/inventory/validation_utils.py +358 -0
- runbooks/inventory/verify_ec2_security_groups.py +18 -5
- runbooks/inventory/vpc_architecture_validator.py +7 -1
- runbooks/inventory/vpc_dependency_analyzer.py +6 -0
- runbooks/main_final.py +2 -2
- runbooks/main_ultra_minimal.py +2 -2
- runbooks/mcp/integration.py +6 -4
- runbooks/remediation/acm_remediation.py +2 -2
- runbooks/remediation/cloudtrail_remediation.py +2 -2
- runbooks/remediation/cognito_remediation.py +2 -2
- runbooks/remediation/dynamodb_remediation.py +2 -2
- runbooks/remediation/ec2_remediation.py +2 -2
- runbooks/remediation/kms_remediation.py +2 -2
- runbooks/remediation/lambda_remediation.py +2 -2
- runbooks/remediation/rds_remediation.py +2 -2
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/vpc/cloudtrail_audit_integration.py +1 -1
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/RECORD +106 -100
- runbooks/__init__.py.backup +0 -134
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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:
|
runbooks/inventory/discovery.md
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
48
|
-
@click.
|
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
|
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
|
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
|
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
|
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
|
-
|
79
|
+
from runbooks.common.rich_utils import console
|
80
|
+
from runbooks import __version__
|
80
81
|
|
81
|
-
#
|
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
|
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
|
|