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
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tag coverage analysis and Rich CLI status display for AWS Organizations.
4
+
5
+ Provides visual feedback on AWS tag coverage across organization accounts
6
+ with tier-based analysis, actionable recommendations, and configurable
7
+ tag mapping validation.
8
+
9
+ Features:
10
+ - Tier-based tag coverage analysis (TIER 1-4)
11
+ - Rich CLI status displays with color-coded indicators
12
+ - Configurable tag mapping validation
13
+ - Multi-tenant portability across Landing Zone configurations
14
+ - Actionable recommendations for tag standardization
15
+
16
+ Architecture:
17
+ - Hierarchical config precedence: CLI > env > project > user > defaults
18
+ - Zero-coverage detection for misconfigured tag mappings
19
+ - Professional Rich formatting with CloudOps theme
20
+
21
+ Author: CloudOps Runbooks Team
22
+ Version: 1.1.10
23
+ """
24
+
25
+ import logging
26
+ from collections import defaultdict
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ from rich import box
30
+ from rich.panel import Panel
31
+ from rich.table import Table
32
+ from rich.text import Text
33
+
34
+ from runbooks.common.rich_utils import STATUS_INDICATORS, console, print_header
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class TagCoverageAnalyzer:
40
+ """
41
+ Analyze and display tag coverage across AWS accounts.
42
+
43
+ Provides comprehensive tag coverage analysis with:
44
+ - Per-field coverage statistics (populated vs total accounts)
45
+ - Tier-level coverage analysis (all fields in tier must be populated)
46
+ - Visual Rich CLI status with color-coded indicators
47
+ - Actionable recommendations for tag standardization
48
+
49
+ Coverage Thresholds:
50
+ ≥90% = Excellent (Green ✅)
51
+ 80-89% = Good (Yellow ⚠️)
52
+ 50-79% = Medium (Orange ⚠️)
53
+ <50% = Poor (Red ❌)
54
+
55
+ Tag Tiers:
56
+ TIER 1: Business Metadata (4 critical fields)
57
+ TIER 2: Governance Metadata (4 important fields)
58
+ TIER 3: Operational Metadata (4 standard fields)
59
+ TIER 4: Extended Metadata (5 optional fields)
60
+
61
+ Example:
62
+ >>> accounts = [
63
+ ... {'id': '123456789012', 'wbs_code': 'WBS-001', 'cost_group': 'Engineering'},
64
+ ... {'id': '234567890123', 'wbs_code': 'N/A', 'cost_group': 'Finance'}
65
+ ... ]
66
+ >>> tag_mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
67
+ >>> analyzer = TagCoverageAnalyzer(accounts, tag_mappings, logger)
68
+ >>> analyzer.display_coverage_status()
69
+ """
70
+
71
+ # Tag tier definitions (aligned with config_schema.py)
72
+ TIER_1_FIELDS = ["wbs_code", "cost_group", "technical_lead", "account_owner"]
73
+ TIER_2_FIELDS = ["business_unit", "functional_area", "managed_by", "product_owner"]
74
+ TIER_3_FIELDS = ["purpose", "environment", "compliance_scope", "data_classification"]
75
+ TIER_4_FIELDS = ["project_name", "budget_code", "support_tier", "created_date", "expiry_date"]
76
+
77
+ # Coverage thresholds
78
+ COVERAGE_EXCELLENT = 90.0 # ≥90% = Green ✅
79
+ COVERAGE_GOOD = 80.0 # 80-89% = Yellow ⚠️
80
+ COVERAGE_MEDIUM = 50.0 # 50-79% = Orange ⚠️
81
+ # <50% = Red ❌
82
+
83
+ def __init__(self, accounts: List[Dict[str, Any]], tag_mappings: Dict[str, str], logger: logging.Logger):
84
+ """
85
+ Initialize coverage analyzer.
86
+
87
+ Args:
88
+ accounts: List of account dicts with tag data (must include tag-based fields)
89
+ tag_mappings: Field name → AWS tag key mappings (from ConfigLoader)
90
+ logger: Logger instance for diagnostic output
91
+
92
+ Example:
93
+ >>> accounts = [{'id': '123', 'wbs_code': 'WBS-001'}]
94
+ >>> mappings = {'wbs_code': 'WBS'}
95
+ >>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
96
+ """
97
+ self.accounts = accounts
98
+ self.tag_mappings = tag_mappings
99
+ self.logger = logger
100
+ self.coverage_stats = self._calculate_coverage()
101
+
102
+ def _calculate_coverage(self) -> Dict[str, Any]:
103
+ """
104
+ Calculate tag coverage statistics per field.
105
+
106
+ Computes:
107
+ 1. Per-field coverage: percentage of accounts with populated tags
108
+ 2. Tier-level coverage: percentage of accounts with ALL tier fields populated
109
+
110
+ Returns:
111
+ Dictionary containing:
112
+ - Per-field stats: {field_name: {populated, total, percentage, tag_key}}
113
+ - Tier stats: {tier_X_overall: {populated, total, percentage}}
114
+
115
+ Example:
116
+ >>> analyzer = TagCoverageAnalyzer([{'id': '123', 'wbs_code': 'WBS-001'}],
117
+ ... {'wbs_code': 'WBS'}, logger)
118
+ >>> stats = analyzer._calculate_coverage()
119
+ >>> 'wbs_code' in stats
120
+ True
121
+ """
122
+ total_accounts = len(self.accounts)
123
+ if total_accounts == 0:
124
+ self.logger.warning("No accounts provided for tag coverage analysis")
125
+ return {}
126
+
127
+ coverage = {}
128
+
129
+ # Calculate per-field coverage
130
+ for field_name in self.tag_mappings.keys():
131
+ populated_count = sum(
132
+ 1 for account in self.accounts if account.get(field_name) not in ["N/A", None, ""]
133
+ )
134
+
135
+ coverage_pct = (populated_count / total_accounts) * 100
136
+
137
+ coverage[field_name] = {
138
+ "populated": populated_count,
139
+ "total": total_accounts,
140
+ "percentage": coverage_pct,
141
+ "tag_key": self.tag_mappings[field_name],
142
+ }
143
+
144
+ # Calculate tier-level coverage (all fields in tier populated)
145
+ coverage["tier_1_overall"] = self._calculate_tier_coverage(self.TIER_1_FIELDS)
146
+ coverage["tier_2_overall"] = self._calculate_tier_coverage(self.TIER_2_FIELDS)
147
+
148
+ self.logger.info(
149
+ f"Tag coverage calculated: {total_accounts} accounts, "
150
+ f"TIER 1: {coverage['tier_1_overall']['percentage']:.1f}%, "
151
+ f"TIER 2: {coverage['tier_2_overall']['percentage']:.1f}%"
152
+ )
153
+
154
+ return coverage
155
+
156
+ def _calculate_tier_coverage(self, tier_fields: List[str]) -> Dict[str, Any]:
157
+ """
158
+ Calculate overall coverage for a tier (all fields populated).
159
+
160
+ For an account to have complete tier coverage, ALL fields in that tier
161
+ must be populated (not 'N/A', None, or empty string).
162
+
163
+ Args:
164
+ tier_fields: List of field names in this tier
165
+
166
+ Returns:
167
+ Dictionary with populated count, total accounts, and percentage
168
+
169
+ Example:
170
+ >>> analyzer = TagCoverageAnalyzer(
171
+ ... [{'wbs_code': 'WBS-001', 'cost_group': 'Eng', 'technical_lead': 'N/A'}],
172
+ ... {'wbs_code': 'WBS', 'cost_group': 'CostGroup', 'technical_lead': 'TechLead'},
173
+ ... logger
174
+ ... )
175
+ >>> tier_coverage = analyzer._calculate_tier_coverage(['wbs_code', 'cost_group'])
176
+ >>> tier_coverage['percentage']
177
+ 100.0
178
+ """
179
+ total_accounts = len(self.accounts)
180
+ if total_accounts == 0:
181
+ return {"populated": 0, "total": 0, "percentage": 0.0}
182
+
183
+ # Count accounts with ALL tier fields populated
184
+ fully_tagged_count = sum(
185
+ 1
186
+ for account in self.accounts
187
+ if all(
188
+ account.get(field) not in ["N/A", None, ""]
189
+ for field in tier_fields
190
+ if field in self.tag_mappings
191
+ )
192
+ )
193
+
194
+ coverage_pct = (fully_tagged_count / total_accounts) * 100
195
+
196
+ return {"populated": fully_tagged_count, "total": total_accounts, "percentage": coverage_pct}
197
+
198
+ def _get_coverage_indicator(self, percentage: float) -> str:
199
+ """
200
+ Get Rich status indicator for coverage percentage.
201
+
202
+ Args:
203
+ percentage: Coverage percentage (0-100)
204
+
205
+ Returns:
206
+ Status indicator emoji (✅ ⚠️ ❌)
207
+
208
+ Example:
209
+ >>> analyzer = TagCoverageAnalyzer([], {}, logger)
210
+ >>> analyzer._get_coverage_indicator(95.0)
211
+ '🟢'
212
+ """
213
+ if percentage >= self.COVERAGE_EXCELLENT:
214
+ return STATUS_INDICATORS["success"] # ✅
215
+ elif percentage >= self.COVERAGE_GOOD:
216
+ return STATUS_INDICATORS["warning"] # ⚠️
217
+ elif percentage >= self.COVERAGE_MEDIUM:
218
+ return STATUS_INDICATORS["warning"] # ⚠️
219
+ else:
220
+ return STATUS_INDICATORS["error"] # ❌
221
+
222
+ def _get_coverage_color(self, percentage: float) -> str:
223
+ """
224
+ Get Rich color for coverage percentage.
225
+
226
+ Args:
227
+ percentage: Coverage percentage (0-100)
228
+
229
+ Returns:
230
+ Rich color name (green, yellow, orange, red)
231
+
232
+ Example:
233
+ >>> analyzer = TagCoverageAnalyzer([], {}, logger)
234
+ >>> analyzer._get_coverage_color(85.0)
235
+ 'yellow'
236
+ """
237
+ if percentage >= self.COVERAGE_EXCELLENT:
238
+ return "green"
239
+ elif percentage >= self.COVERAGE_GOOD:
240
+ return "yellow"
241
+ elif percentage >= self.COVERAGE_MEDIUM:
242
+ return "orange"
243
+ else:
244
+ return "red"
245
+
246
+ def display_coverage_status(self, config_source: str = "hierarchical config") -> None:
247
+ """
248
+ Display tag coverage status with Rich formatting.
249
+
250
+ Creates professional Rich CLI display with:
251
+ - Per-field coverage table (TIER 1 & 2 fields)
252
+ - Color-coded status indicators
253
+ - Overall TIER 1 coverage summary
254
+ - Top 3 actionable recommendations
255
+
256
+ Args:
257
+ config_source: Description of configuration source for display
258
+
259
+ Example:
260
+ >>> accounts = [{'wbs_code': 'WBS-001', 'cost_group': 'Engineering'}]
261
+ >>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
262
+ >>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
263
+ >>> analyzer.display_coverage_status("user config")
264
+ """
265
+ # Build coverage table
266
+ table = Table(title="Tag Coverage Analysis", box=box.ROUNDED, show_header=True, header_style="bold cyan")
267
+
268
+ table.add_column("Field Name", style="white", width=20)
269
+ table.add_column("AWS Tag Key", style="cyan", width=20)
270
+ table.add_column("Coverage", justify="right", style="white", width=15)
271
+ table.add_column("Status", justify="center", width=8)
272
+
273
+ # TIER 1: Business Metadata
274
+ table.add_row(
275
+ Text("TIER 1: Business Metadata", style="bold yellow"), "", "", "", style="bold yellow"
276
+ )
277
+ for field in self.TIER_1_FIELDS:
278
+ if field in self.coverage_stats:
279
+ self._add_coverage_row(table, field)
280
+
281
+ # TIER 2: Governance Metadata
282
+ table.add_row("", "", "", "") # Spacer
283
+ table.add_row(
284
+ Text("TIER 2: Governance Metadata", style="bold magenta"), "", "", "", style="bold magenta"
285
+ )
286
+ for field in self.TIER_2_FIELDS:
287
+ if field in self.coverage_stats:
288
+ self._add_coverage_row(table, field)
289
+
290
+ # Build summary
291
+ tier1_coverage = self.coverage_stats["tier_1_overall"]["percentage"]
292
+ tier1_indicator = self._get_coverage_indicator(tier1_coverage)
293
+ tier1_color = self._get_coverage_color(tier1_coverage)
294
+
295
+ summary_text = Text()
296
+ summary_text.append("Configuration Source: ", style="bold white")
297
+ summary_text.append(f"{config_source}\n\n", style="cyan")
298
+
299
+ summary_text.append("TIER 1 Overall Coverage: ", style="bold white")
300
+ summary_text.append(f"{tier1_coverage:.1f}% {tier1_indicator}", style=tier1_color)
301
+ summary_text.append(
302
+ f" ({self.coverage_stats['tier_1_overall']['populated']}/"
303
+ f"{self.coverage_stats['tier_1_overall']['total']} accounts)\n",
304
+ style="white",
305
+ )
306
+
307
+ # Add recommendations
308
+ recommendations = self._generate_recommendations()
309
+ if recommendations:
310
+ summary_text.append("\n💡 Recommendations:\n", style="bold yellow")
311
+ for rec in recommendations[:3]: # Top 3
312
+ summary_text.append(f" • {rec}\n", style="white")
313
+
314
+ # Display panel
315
+ panel = Panel(
316
+ table,
317
+ title="[bold cyan]Tag Mapping Status[/bold cyan]",
318
+ subtitle=summary_text,
319
+ border_style="cyan",
320
+ box=box.DOUBLE,
321
+ )
322
+
323
+ console.print("\n")
324
+ console.print(panel)
325
+ console.print("\n")
326
+
327
+ def _add_coverage_row(self, table: Table, field_name: str) -> None:
328
+ """
329
+ Add coverage row to table.
330
+
331
+ Args:
332
+ table: Rich Table instance to add row to
333
+ field_name: Field name to add row for
334
+
335
+ Example:
336
+ >>> table = Table()
337
+ >>> analyzer = TagCoverageAnalyzer(
338
+ ... [{'wbs_code': 'WBS-001'}], {'wbs_code': 'WBS'}, logger
339
+ ... )
340
+ >>> analyzer._add_coverage_row(table, 'wbs_code')
341
+ """
342
+ stats = self.coverage_stats[field_name]
343
+ percentage = stats["percentage"]
344
+ indicator = self._get_coverage_indicator(percentage)
345
+ color = self._get_coverage_color(percentage)
346
+
347
+ coverage_text = f"{stats['populated']}/{stats['total']} ({percentage:.1f}%)"
348
+
349
+ table.add_row(f" {field_name}", stats["tag_key"], Text(coverage_text, style=color), indicator)
350
+
351
+ def _generate_recommendations(self) -> List[str]:
352
+ """
353
+ Generate actionable recommendations based on coverage.
354
+
355
+ Analyzes coverage gaps and provides specific recommendations for:
356
+ 1. Low-coverage critical fields (TIER 1 <80%)
357
+ 2. Overall TIER 1 coverage below target
358
+ 3. Zero-coverage fields (potential configuration errors)
359
+
360
+ Returns:
361
+ List of recommendation strings (ordered by priority)
362
+
363
+ Example:
364
+ >>> accounts = [{'wbs_code': 'N/A', 'cost_group': 'N/A'}]
365
+ >>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
366
+ >>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
367
+ >>> recommendations = analyzer._generate_recommendations()
368
+ >>> len(recommendations) > 0
369
+ True
370
+ """
371
+ recommendations = []
372
+
373
+ # Check TIER 1 critical fields
374
+ for field in self.TIER_1_FIELDS:
375
+ if field in self.coverage_stats:
376
+ stats = self.coverage_stats[field]
377
+ if stats["percentage"] < self.COVERAGE_GOOD:
378
+ missing_count = stats["total"] - stats["populated"]
379
+ recommendations.append(
380
+ f"Review '{field}' tag configuration - "
381
+ f"{missing_count} accounts missing '{stats['tag_key']}' tag"
382
+ )
383
+
384
+ # Check overall TIER 1 coverage
385
+ tier1_coverage = self.coverage_stats["tier_1_overall"]["percentage"]
386
+ if tier1_coverage < self.COVERAGE_GOOD:
387
+ recommendations.append(
388
+ f"Overall TIER 1 coverage is {tier1_coverage:.1f}% (target: ≥80%). "
389
+ "Consider standardizing tag naming across AWS accounts."
390
+ )
391
+
392
+ # Check for zero coverage (wrong tag mapping?)
393
+ zero_coverage_fields = [
394
+ field
395
+ for field, stats in self.coverage_stats.items()
396
+ if isinstance(stats, dict) and stats.get("percentage", 100) == 0 and field in self.TIER_1_FIELDS
397
+ ]
398
+ if zero_coverage_fields:
399
+ recommendations.append(
400
+ f"Zero coverage for: {', '.join(zero_coverage_fields)}. "
401
+ "Verify tag mapping configuration matches AWS tag keys."
402
+ )
403
+
404
+ return recommendations
405
+
406
+ def get_coverage_summary(self) -> Dict[str, Any]:
407
+ """
408
+ Get coverage summary for programmatic access.
409
+
410
+ Returns:
411
+ Dictionary containing:
412
+ - total_accounts: Number of accounts analyzed
413
+ - tier_1_coverage: TIER 1 overall coverage percentage
414
+ - tier_2_coverage: TIER 2 overall coverage percentage
415
+ - field_coverage: Per-field coverage percentages
416
+ - recommendations: List of actionable recommendations
417
+
418
+ Example:
419
+ >>> accounts = [{'wbs_code': 'WBS-001'}]
420
+ >>> mappings = {'wbs_code': 'WBS'}
421
+ >>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
422
+ >>> summary = analyzer.get_coverage_summary()
423
+ >>> 'total_accounts' in summary
424
+ True
425
+ """
426
+ return {
427
+ "total_accounts": len(self.accounts),
428
+ "tier_1_coverage": self.coverage_stats["tier_1_overall"]["percentage"],
429
+ "tier_2_coverage": self.coverage_stats["tier_2_overall"]["percentage"],
430
+ "field_coverage": {
431
+ field: stats["percentage"]
432
+ for field, stats in self.coverage_stats.items()
433
+ if isinstance(stats, dict) and "percentage" in stats
434
+ },
435
+ "recommendations": self._generate_recommendations(),
436
+ }
437
+
438
+
439
+ def display_tag_coverage_status(
440
+ accounts: List[Dict[str, Any]],
441
+ tag_mappings: Dict[str, str],
442
+ config_source: str,
443
+ logger: logging.Logger,
444
+ ) -> None:
445
+ """
446
+ Display tag coverage statistics with Rich formatting (convenience function).
447
+
448
+ Creates TagCoverageAnalyzer and displays professional Rich CLI status.
449
+ This is the primary public API for tag coverage display.
450
+
451
+ Args:
452
+ accounts: List of account dicts with tag data
453
+ tag_mappings: Field name → AWS tag key mappings
454
+ config_source: Description of config source (e.g., "user config", "CLI overrides")
455
+ logger: Logger instance
456
+
457
+ Example:
458
+ >>> accounts = [
459
+ ... {'id': '123456789012', 'wbs_code': 'WBS-001', 'cost_group': 'Engineering'},
460
+ ... {'id': '234567890123', 'wbs_code': 'WBS-002', 'cost_group': 'Finance'}
461
+ ... ]
462
+ >>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
463
+ >>> display_tag_coverage_status(accounts, mappings, "defaults", logger)
464
+ """
465
+ analyzer = TagCoverageAnalyzer(accounts, tag_mappings, logger)
466
+ analyzer.display_coverage_status(config_source)
467
+
468
+ # Log coverage summary
469
+ summary = analyzer.get_coverage_summary()
470
+ logger.info(
471
+ f"Tag coverage analysis complete: "
472
+ f"TIER 1: {summary['tier_1_coverage']:.1f}%, "
473
+ f"Total accounts: {summary['total_accounts']}"
474
+ )
475
+
476
+
477
+ # Module exports
478
+ __all__ = [
479
+ "TagCoverageAnalyzer",
480
+ "display_tag_coverage_status",
481
+ ]