iam-policy-validator 1.14.0__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 (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,171 @@
1
+ """CSV formatter for IAM Policy Validator."""
2
+
3
+ import csv
4
+ import io
5
+ from typing import Any
6
+
7
+ from iam_validator.core import constants
8
+ from iam_validator.core.formatters.base import OutputFormatter
9
+ from iam_validator.core.models import ValidationReport
10
+
11
+
12
+ class CSVFormatter(OutputFormatter):
13
+ """Formats validation results as CSV for spreadsheet analysis."""
14
+
15
+ @property
16
+ def format_id(self) -> str:
17
+ return "csv"
18
+
19
+ @property
20
+ def description(self) -> str:
21
+ return "CSV format for spreadsheet import and analysis"
22
+
23
+ @property
24
+ def file_extension(self) -> str:
25
+ return "csv"
26
+
27
+ @property
28
+ def content_type(self) -> str:
29
+ return "text/csv"
30
+
31
+ def format(self, report: ValidationReport, **kwargs) -> str:
32
+ """Format report as CSV.
33
+
34
+ Args:
35
+ report: The validation report
36
+ **kwargs: Additional options like 'include_summary'
37
+
38
+ Returns:
39
+ CSV string
40
+ """
41
+ include_summary = kwargs.get("include_summary", True)
42
+ include_header = kwargs.get("include_header", True)
43
+
44
+ output = io.StringIO()
45
+ writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
46
+
47
+ if include_summary:
48
+ self._write_summary_section(writer, report)
49
+ writer.writerow([]) # Empty row separator
50
+
51
+ self._write_issues_section(writer, report, include_header)
52
+
53
+ return output.getvalue()
54
+
55
+ def _write_summary_section(self, writer: Any, report: ValidationReport) -> None:
56
+ """Write summary statistics to CSV."""
57
+ # Count issues by severity - support both IAM validity and security severities
58
+ errors = sum(
59
+ 1
60
+ for r in report.results
61
+ for i in r.issues
62
+ if i.severity in constants.HIGH_SEVERITY_LEVELS
63
+ )
64
+ warnings = sum(
65
+ 1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
66
+ )
67
+ infos = sum(1 for r in report.results for i in r.issues if i.severity in ("info", "low"))
68
+
69
+ writer.writerow(["Summary Statistics"])
70
+ writer.writerow(["Metric", "Value"])
71
+ writer.writerow(["Total Policies", report.total_policies])
72
+ writer.writerow(["Valid Policies (IAM)", report.valid_policies])
73
+ writer.writerow(["Invalid Policies (IAM)", report.invalid_policies])
74
+ writer.writerow(["Policies with Security Findings", report.policies_with_security_issues])
75
+ writer.writerow(["Total Issues", report.total_issues])
76
+ writer.writerow(["Validity Issues", report.validity_issues])
77
+ writer.writerow(["Security Issues", report.security_issues])
78
+ writer.writerow([""])
79
+ writer.writerow(["Legacy Severity Breakdown"])
80
+ writer.writerow(["Errors", errors])
81
+ writer.writerow(["Warnings", warnings])
82
+ writer.writerow(["Info", infos])
83
+
84
+ def _write_issues_section(
85
+ self, writer: Any, report: ValidationReport, include_header: bool
86
+ ) -> None:
87
+ """Write detailed issues to CSV."""
88
+ if include_header:
89
+ writer.writerow(
90
+ [
91
+ "Policy File",
92
+ "Statement Index",
93
+ "Statement SID",
94
+ "Line Number",
95
+ "Severity",
96
+ "Issue Type",
97
+ "Action",
98
+ "Resource",
99
+ "Condition Key",
100
+ "Message",
101
+ "Suggestion",
102
+ ]
103
+ )
104
+
105
+ for policy_result in report.results:
106
+ for issue in policy_result.issues:
107
+ writer.writerow(
108
+ [
109
+ policy_result.policy_file,
110
+ (issue.statement_index + 1 if issue.statement_index is not None else ""),
111
+ issue.statement_sid or "",
112
+ issue.line_number or "",
113
+ issue.severity,
114
+ issue.issue_type or "",
115
+ issue.action or "",
116
+ issue.resource or "",
117
+ issue.condition_key or "",
118
+ issue.message,
119
+ issue.suggestion or "",
120
+ ]
121
+ )
122
+
123
+ def format_pivot_table(self, report: ValidationReport) -> str:
124
+ """Format report as a pivot table CSV for analysis.
125
+
126
+ Groups issues by check_id and severity for easy analysis.
127
+ """
128
+ output = io.StringIO()
129
+ writer = csv.writer(output)
130
+
131
+ # Create pivot data
132
+ pivot_data = self._create_pivot_data(report)
133
+
134
+ # Write header
135
+ writer.writerow(["Issue Type", "Severity", "Count", "Policy Files"])
136
+
137
+ # Write pivot rows
138
+ for (issue_type, severity), data in sorted(pivot_data.items()):
139
+ writer.writerow(
140
+ [
141
+ issue_type or "unknown",
142
+ severity,
143
+ data["count"],
144
+ "; ".join(data["files"][:5]) + ("..." if len(data["files"]) > 5 else ""),
145
+ ]
146
+ )
147
+
148
+ return output.getvalue()
149
+
150
+ def _create_pivot_data(self, report: ValidationReport) -> dict[tuple, dict[str, Any]]:
151
+ """Create pivot table data structure."""
152
+ pivot_data = {}
153
+
154
+ for policy_result in report.results:
155
+ for issue in policy_result.issues:
156
+ key = (issue.issue_type or "unknown", issue.severity)
157
+
158
+ if key not in pivot_data:
159
+ pivot_data[key] = {
160
+ "count": 0,
161
+ "files": set(),
162
+ }
163
+
164
+ pivot_data[key]["count"] += 1
165
+ pivot_data[key]["files"].add(policy_result.policy_file)
166
+
167
+ # Convert sets to lists
168
+ for key in pivot_data:
169
+ pivot_data[key]["files"] = sorted(list(pivot_data[key]["files"]))
170
+
171
+ return pivot_data
@@ -0,0 +1,481 @@
1
+ """Enhanced formatter - Rich-based console output with modern design."""
2
+
3
+ from io import StringIO
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.rule import Rule
8
+ from rich.table import Table
9
+ from rich.text import Text
10
+ from rich.tree import Tree
11
+
12
+ from iam_validator.__version__ import __version__
13
+ from iam_validator.core import constants
14
+ from iam_validator.core.formatters.base import OutputFormatter
15
+ from iam_validator.core.models import PolicyValidationResult, ValidationReport
16
+
17
+
18
+ class EnhancedFormatter(OutputFormatter):
19
+ """Enhanced console formatter with modern, visually rich output using Rich library."""
20
+
21
+ @property
22
+ def format_id(self) -> str:
23
+ return "enhanced"
24
+
25
+ @property
26
+ def description(self) -> str:
27
+ return "Enhanced console output with progress bars, tree structure, and rich visuals"
28
+
29
+ def format(self, report: ValidationReport, **kwargs) -> str:
30
+ """Format validation report as modern Rich console output.
31
+
32
+ This creates a visually enhanced string representation with:
33
+ - Gradient-styled headers
34
+ - Progress bars for validation metrics
35
+ - Tree structure for issues
36
+ - Bordered panels with icons
37
+
38
+ Args:
39
+ report: Validation report to format
40
+ **kwargs: Additional options:
41
+ - color (bool): Enable color output (default: True)
42
+ - show_summary (bool): Show Executive Summary panel (default: True)
43
+ - show_severity_breakdown (bool): Show Issue Severity Breakdown panel (default: True)
44
+
45
+ Returns:
46
+ Formatted string with ANSI codes for console display
47
+ """
48
+ # Allow disabling color for plain text output
49
+ color = kwargs.get("color", True)
50
+ show_summary = kwargs.get("show_summary", True)
51
+ show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
52
+
53
+ # Use StringIO to capture Rich console output
54
+ from iam_validator.utils import get_terminal_width
55
+
56
+ string_buffer = StringIO()
57
+ # Get terminal width for proper text wrapping
58
+ terminal_width = get_terminal_width()
59
+ console = Console(
60
+ file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
61
+ )
62
+
63
+ # Header with title
64
+ title = Text(
65
+ f"IAM Policy Validation Report (v{__version__})",
66
+ style="bold cyan",
67
+ justify="center",
68
+ )
69
+ console.print(
70
+ Panel(
71
+ title,
72
+ border_style=constants.CONSOLE_HEADER_COLOR,
73
+ padding=(1, 0),
74
+ width=constants.CONSOLE_PANEL_WIDTH,
75
+ )
76
+ )
77
+
78
+ # Executive Summary with progress bars (optional)
79
+ if show_summary:
80
+ self._print_summary_panel(console, report)
81
+
82
+ # Severity breakdown if there are issues (optional)
83
+ if show_severity_breakdown and report.total_issues > 0:
84
+ self._print_severity_breakdown(console, report)
85
+
86
+ console.print(
87
+ Rule(
88
+ title="[bold]Detailed Results",
89
+ style=constants.CONSOLE_HEADER_COLOR,
90
+ ),
91
+ width=constants.CONSOLE_PANEL_WIDTH,
92
+ )
93
+
94
+ # Detailed results using tree structure
95
+ for idx, result in enumerate(report.results, 1):
96
+ self._format_policy_result_modern(console, result, idx, len(report.results))
97
+
98
+ # Final status with styled box
99
+ console.print()
100
+ self._print_final_status(console, report)
101
+
102
+ # Get the formatted output
103
+ output = string_buffer.getvalue().rstrip("\n")
104
+ string_buffer.close()
105
+
106
+ return output
107
+
108
+ def _print_summary_panel(self, console: Console, report: ValidationReport) -> None:
109
+ """Print summary panel with clean metrics display."""
110
+ # Create a simple table for metrics without progress bars
111
+ metrics_table = Table.grid(padding=(0, 2))
112
+ metrics_table.add_column(style="bold", justify="left", width=30)
113
+ metrics_table.add_column(style="bold", justify="left", width=20)
114
+
115
+ # Total policies
116
+ metrics_table.add_row(
117
+ "📋 Total Policies",
118
+ str(report.total_policies),
119
+ )
120
+
121
+ # Valid policies
122
+ if report.total_policies > 0:
123
+ valid_pct = report.valid_policies * 100 // report.total_policies
124
+ metrics_table.add_row(
125
+ "✅ Valid Policies",
126
+ f"[green]{report.valid_policies} ({valid_pct}%)[/green]",
127
+ )
128
+
129
+ # Invalid policies
130
+ invalid_pct = report.invalid_policies * 100 // report.total_policies
131
+ if report.invalid_policies > 0:
132
+ metrics_table.add_row(
133
+ "❌ Invalid Policies",
134
+ f"[red]{report.invalid_policies} ({invalid_pct}%)[/red]",
135
+ )
136
+ else:
137
+ metrics_table.add_row(
138
+ "❌ Invalid Policies",
139
+ f"[dim]{report.invalid_policies} ({invalid_pct}%)[/dim]",
140
+ )
141
+
142
+ # Total issues
143
+ if report.total_issues > 0:
144
+ metrics_table.add_row(
145
+ "⚠️ Total Issues Found",
146
+ f"[red]{report.total_issues}[/red]",
147
+ )
148
+ else:
149
+ metrics_table.add_row(
150
+ "⚠️ Total Issues Found",
151
+ f"[green]{report.total_issues}[/green]",
152
+ )
153
+
154
+ console.print(
155
+ Panel(
156
+ metrics_table,
157
+ title="📊 Executive Summary",
158
+ border_style=constants.CONSOLE_HEADER_COLOR,
159
+ padding=(1, 2),
160
+ width=constants.CONSOLE_PANEL_WIDTH,
161
+ )
162
+ )
163
+
164
+ def _create_progress_bar(self, value: int, total: int, color: str) -> str:
165
+ """Create a simple text-based progress bar."""
166
+ if total == 0:
167
+ return "[dim]───────────────────────[/dim]"
168
+
169
+ percentage = min(value * 100 // total, 100)
170
+ filled = int(percentage / 5) # 20 bars total (100/5)
171
+ empty = 20 - filled
172
+
173
+ bar = f"[{color}]{'█' * filled}[/{color}][dim]{'░' * empty}[/dim]"
174
+ return bar
175
+
176
+ def _print_severity_breakdown(self, console: Console, report: ValidationReport) -> None:
177
+ """Print a clean breakdown of issues by severity."""
178
+ # Count issues by severity
179
+ severity_counts = {
180
+ "critical": 0,
181
+ "high": 0,
182
+ "error": 0,
183
+ "medium": 0,
184
+ "warning": 0,
185
+ "low": 0,
186
+ "info": 0,
187
+ }
188
+
189
+ for result in report.results:
190
+ for issue in result.issues:
191
+ severity = issue.severity.lower()
192
+ if severity in severity_counts:
193
+ severity_counts[severity] += 1
194
+
195
+ # Create clean severity table
196
+ severity_table = Table.grid(padding=(0, 2))
197
+ severity_table.add_column(style="bold", justify="left", width=25)
198
+ severity_table.add_column(style="bold", justify="left", width=15)
199
+
200
+ # Show individual severity counts
201
+ if severity_counts["critical"] > 0:
202
+ severity_table.add_row(
203
+ "🔴 Critical",
204
+ f"[red]{severity_counts['critical']}[/red]",
205
+ )
206
+
207
+ if severity_counts["high"] > 0:
208
+ severity_table.add_row(
209
+ "🔴 High",
210
+ f"[red]{severity_counts['high']}[/red]",
211
+ )
212
+
213
+ if severity_counts["error"] > 0:
214
+ severity_table.add_row(
215
+ "🔴 Error",
216
+ f"[red]{severity_counts['error']}[/red]",
217
+ )
218
+
219
+ if severity_counts["medium"] > 0:
220
+ severity_table.add_row(
221
+ "🟡 Medium",
222
+ f"[yellow]{severity_counts['medium']}[/yellow]",
223
+ )
224
+
225
+ if severity_counts["warning"] > 0:
226
+ severity_table.add_row(
227
+ "🟡 Warning",
228
+ f"[yellow]{severity_counts['warning']}[/yellow]",
229
+ )
230
+
231
+ if severity_counts["low"] > 0:
232
+ severity_table.add_row(
233
+ "🔵 Low",
234
+ f"[blue]{severity_counts['low']}[/blue]",
235
+ )
236
+
237
+ if severity_counts["info"] > 0:
238
+ severity_table.add_row(
239
+ "🔵 Info",
240
+ f"[blue]{severity_counts['info']}[/blue]",
241
+ )
242
+
243
+ console.print(
244
+ Panel(
245
+ severity_table,
246
+ title="🎯 Issue Severity Breakdown",
247
+ border_style=constants.CONSOLE_HEADER_COLOR,
248
+ width=constants.CONSOLE_PANEL_WIDTH,
249
+ )
250
+ )
251
+
252
+ def _format_policy_result_modern(
253
+ self, console: Console, result: PolicyValidationResult, idx: int, total: int
254
+ ) -> None:
255
+ """Format policy results with modern tree structure.
256
+
257
+ Args:
258
+ console: Rich console instance
259
+ result: Policy validation result
260
+ idx: Index of this policy (1-based)
261
+ total: Total number of policies
262
+ """
263
+ # Status icon and color
264
+ if result.is_valid and not result.issues:
265
+ icon = "✅"
266
+ color = "green"
267
+ status_text = "VALID"
268
+ elif result.is_valid and result.issues:
269
+ # Valid IAM policy but has security findings
270
+ # Check severity to determine the appropriate status
271
+ has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
272
+ if has_critical:
273
+ icon = "⚠️"
274
+ color = "red"
275
+ status_text = "VALID (with security issues)"
276
+ else:
277
+ icon = "⚠️"
278
+ color = "yellow"
279
+ status_text = "VALID (with warnings)"
280
+ else:
281
+ # Policy failed validation (is_valid=false)
282
+ # Check if it's due to IAM errors or security issues
283
+ has_iam_errors = any(i.severity == "error" for i in result.issues)
284
+ has_security_critical = any(i.severity in ("critical", "high") for i in result.issues)
285
+
286
+ if has_iam_errors and has_security_critical:
287
+ # Both IAM errors and security issues
288
+ status_text = "INVALID (IAM errors + security issues)"
289
+ elif has_iam_errors:
290
+ # Only IAM validation errors
291
+ status_text = "INVALID (IAM errors)"
292
+ else:
293
+ # Only security issues (failed due to fail_on_severity config)
294
+ status_text = "FAILED (critical security issues)"
295
+
296
+ icon = "❌"
297
+ color = "red"
298
+
299
+ # Policy header
300
+ header = Text()
301
+ header.append(f"{icon} ", style=color)
302
+ header.append(f"[{idx}/{total}] ", style="dim")
303
+ header.append(result.policy_file, style=f"bold {color}")
304
+ header.append(f" • {status_text}", style=f"{color}")
305
+
306
+ if not result.issues:
307
+ console.print(header)
308
+ console.print(" [dim italic]No issues detected[/dim italic]")
309
+ return
310
+
311
+ console.print(header)
312
+ console.print(f" [dim]{len(result.issues)} issue(s) found[/dim]")
313
+ # Create tree structure for issues
314
+ tree = Tree(f"[bold]Issues ({len(result.issues)})[/bold]", guide_style="bright_black")
315
+
316
+ # Group issues by severity with proper categorization
317
+ critical_issues = [i for i in result.issues if i.severity == "critical"]
318
+ high_issues = [i for i in result.issues if i.severity == "high"]
319
+ error_issues = [i for i in result.issues if i.severity == "error"]
320
+ medium_issues = [i for i in result.issues if i.severity == "medium"]
321
+ warning_issues = [i for i in result.issues if i.severity == "warning"]
322
+ low_issues = [i for i in result.issues if i.severity == "low"]
323
+ info_issues = [i for i in result.issues if i.severity == "info"]
324
+
325
+ # Add critical issues (security checks)
326
+ if critical_issues:
327
+ critical_branch = tree.add("🔴 [bold red]Critical[/bold red]")
328
+ for issue in critical_issues:
329
+ self._add_issue_to_tree(critical_branch, issue, "red")
330
+
331
+ # Add high severity issues (security checks)
332
+ if high_issues:
333
+ high_branch = tree.add("🔴 [bold red]High[/bold red]")
334
+ for issue in high_issues:
335
+ self._add_issue_to_tree(high_branch, issue, "red")
336
+
337
+ # Add errors (IAM validation)
338
+ if error_issues:
339
+ error_branch = tree.add("🔴 [bold red]Error[/bold red]")
340
+ for issue in error_issues:
341
+ self._add_issue_to_tree(error_branch, issue, "red")
342
+
343
+ # Add medium severity issues (security checks)
344
+ if medium_issues:
345
+ medium_branch = tree.add("🟡 [bold yellow]Medium[/bold yellow]")
346
+ for issue in medium_issues:
347
+ self._add_issue_to_tree(medium_branch, issue, "yellow")
348
+
349
+ # Add warnings (IAM validation)
350
+ if warning_issues:
351
+ warning_branch = tree.add("🟡 [bold yellow]Warning[/bold yellow]")
352
+ for issue in warning_issues:
353
+ self._add_issue_to_tree(warning_branch, issue, "yellow")
354
+
355
+ # Add low severity issues (security checks)
356
+ if low_issues:
357
+ low_branch = tree.add("🔵 [bold blue]Low[/bold blue]")
358
+ for issue in low_issues:
359
+ self._add_issue_to_tree(low_branch, issue, "blue")
360
+
361
+ # Add info (IAM validation)
362
+ if info_issues:
363
+ info_branch = tree.add("🔵 [bold blue]Info[/bold blue]")
364
+ for issue in info_issues:
365
+ self._add_issue_to_tree(info_branch, issue, "blue")
366
+
367
+ console.print(" ", tree)
368
+ console.print()
369
+
370
+ def _add_issue_to_tree(self, branch: Tree, issue, color: str) -> None:
371
+ """Add an issue to a tree branch."""
372
+ # Build location string (use 1-indexed statement numbers for user-facing output)
373
+ # Handle policy-level issues (statement_index = -1)
374
+ if issue.statement_index == -1:
375
+ location = "Policy-level"
376
+ else:
377
+ statement_num = issue.statement_index + 1
378
+ location = f"Statement {statement_num}"
379
+ if issue.statement_sid:
380
+ location = f"{issue.statement_sid} (#{statement_num})"
381
+ if issue.line_number is not None:
382
+ location += f" @L{issue.line_number}"
383
+
384
+ # Issue summary
385
+ issue_text = Text()
386
+ issue_text.append(f"[{location}] ", style="dim")
387
+ issue_text.append(issue.issue_type, style=f"bold {color}")
388
+ issue_node = branch.add(issue_text)
389
+
390
+ # Message
391
+ msg_node = issue_node.add(Text(issue.message, style="white"))
392
+
393
+ # Details
394
+ if issue.action or issue.resource or issue.condition_key:
395
+ details = []
396
+ if issue.action:
397
+ details.append(f"Action: {issue.action}")
398
+ if issue.resource:
399
+ details.append(f"Resource: {issue.resource}")
400
+ if issue.condition_key:
401
+ details.append(f"Condition: {issue.condition_key}")
402
+ msg_node.add(Text(" • ".join(details), style="dim cyan"))
403
+
404
+ # Suggestion and Example - combine into single node to reduce spacing
405
+ if issue.suggestion or issue.example:
406
+ combined_text = Text()
407
+
408
+ # Add suggestion
409
+ if issue.suggestion:
410
+ combined_text.append("💡 ", style="yellow")
411
+ combined_text.append(issue.suggestion, style="italic yellow")
412
+
413
+ # Add example on same node (reduces vertical spacing)
414
+ if issue.example:
415
+ if issue.suggestion:
416
+ combined_text.append("\n", style="yellow") # Single newline separator
417
+ combined_text.append("Example:", style="bold cyan")
418
+ combined_text.append("\n")
419
+ combined_text.append(issue.example, style="dim")
420
+
421
+ msg_node.add(combined_text)
422
+
423
+ def _print_final_status(self, console: Console, report: ValidationReport) -> None:
424
+ """Print final status panel."""
425
+ if report.invalid_policies == 0 and report.total_issues == 0:
426
+ # Perfect success
427
+ status = Text("🎉 ALL POLICIES VALIDATED SUCCESSFULLY! 🎉", style="bold green")
428
+ message = Text(
429
+ f"All {report.valid_policies} policies passed validation with no issues.",
430
+ style="green",
431
+ )
432
+ border_color = "green"
433
+ elif report.invalid_policies == 0:
434
+ # Valid IAM policies but may have security findings
435
+ # Check if there are critical/high security issues
436
+ has_critical = any(
437
+ i.severity in constants.HIGH_SEVERITY_LEVELS
438
+ for r in report.results
439
+ for i in r.issues
440
+ )
441
+
442
+ if has_critical:
443
+ status = Text("⚠️ All Policies Valid (with security issues)", style="bold red")
444
+ message = Text(
445
+ f"{report.valid_policies} policies are valid, but {report.total_issues} "
446
+ f"security issue(s) were found that must be addressed.",
447
+ style="red",
448
+ )
449
+ border_color = "red"
450
+ else:
451
+ status = Text("✅ All Policies Valid (with warnings)", style="bold yellow")
452
+ message = Text(
453
+ f"{report.valid_policies} policies are valid, but {report.total_issues} "
454
+ f"warning(s) were found that should be reviewed.",
455
+ style="yellow",
456
+ )
457
+ border_color = "yellow"
458
+ else:
459
+ # Has invalid policies
460
+ status = Text("❌ VALIDATION FAILED", style="bold red")
461
+ message = Text(
462
+ f"{report.invalid_policies} of {report.total_policies} policies have critical "
463
+ f"issues that must be resolved.",
464
+ style="red",
465
+ )
466
+ border_color = "red"
467
+
468
+ # Combine status and message
469
+ final_text = Text()
470
+ final_text.append(status)
471
+ final_text.append("\n") # Reduced from \n\n to single newline
472
+ final_text.append(message)
473
+
474
+ console.print(
475
+ Panel(
476
+ final_text,
477
+ border_style=border_color,
478
+ padding=(1, 2),
479
+ width=constants.CONSOLE_PANEL_WIDTH,
480
+ )
481
+ )