iam-policy-validator 1.0.4__py3-none-any.whl → 1.1.1__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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +88 -10
- iam_policy_validator-1.1.1.dist-info/RECORD +53 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_condition_enforcement.py +112 -28
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +18 -138
- iam_validator/checks/security_best_practices.py +241 -400
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +89 -0
- iam_validator/commands/__init__.py +3 -1
- iam_validator/commands/cache.py +402 -0
- iam_validator/commands/validate.py +7 -5
- iam_validator/core/access_analyzer_report.py +2 -1
- iam_validator/core/aws_fetcher.py +79 -19
- iam_validator/core/check_registry.py +3 -0
- iam_validator/core/cli.py +1 -1
- iam_validator/core/config_loader.py +40 -3
- iam_validator/core/defaults.py +334 -0
- iam_validator/core/formatters/__init__.py +2 -0
- iam_validator/core/formatters/console.py +44 -7
- iam_validator/core/formatters/csv.py +7 -2
- iam_validator/core/formatters/enhanced.py +433 -0
- iam_validator/core/formatters/html.py +127 -37
- iam_validator/core/formatters/markdown.py +10 -2
- iam_validator/core/models.py +30 -6
- iam_validator/core/policy_checks.py +21 -2
- iam_validator/core/report.py +112 -26
- iam_policy_validator-1.0.4.dist-info/RECORD +0 -45
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,433 @@
|
|
|
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.formatters.base import OutputFormatter
|
|
14
|
+
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnhancedFormatter(OutputFormatter):
|
|
18
|
+
"""Enhanced console formatter with modern, visually rich output using Rich library."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def format_id(self) -> str:
|
|
22
|
+
return "enhanced"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
return "Enhanced console output with progress bars, tree structure, and rich visuals"
|
|
27
|
+
|
|
28
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
29
|
+
"""Format validation report as modern Rich console output.
|
|
30
|
+
|
|
31
|
+
This creates a visually enhanced string representation with:
|
|
32
|
+
- Gradient-styled headers
|
|
33
|
+
- Progress bars for validation metrics
|
|
34
|
+
- Tree structure for issues
|
|
35
|
+
- Bordered panels with icons
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
report: Validation report to format
|
|
39
|
+
**kwargs: Additional options (color: bool = True)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Formatted string with ANSI codes for console display
|
|
43
|
+
"""
|
|
44
|
+
# Allow disabling color for plain text output
|
|
45
|
+
color = kwargs.get("color", True)
|
|
46
|
+
|
|
47
|
+
# Use StringIO to capture Rich console output
|
|
48
|
+
string_buffer = StringIO()
|
|
49
|
+
console = Console(file=string_buffer, force_terminal=color, width=120)
|
|
50
|
+
|
|
51
|
+
# Header with title
|
|
52
|
+
console.print()
|
|
53
|
+
title = Text(
|
|
54
|
+
f"IAM Policy Validation Report (v{__version__})",
|
|
55
|
+
style="bold cyan",
|
|
56
|
+
justify="center",
|
|
57
|
+
)
|
|
58
|
+
console.print(Panel(title, border_style="bright_blue", padding=(1, 0)))
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
# Executive Summary with progress bars
|
|
62
|
+
self._print_summary_panel(console, report)
|
|
63
|
+
|
|
64
|
+
# Severity breakdown if there are issues
|
|
65
|
+
if report.total_issues > 0:
|
|
66
|
+
console.print()
|
|
67
|
+
self._print_severity_breakdown(console, report)
|
|
68
|
+
|
|
69
|
+
console.print()
|
|
70
|
+
console.print(Rule(title="[bold]Detailed Results", style="bright_blue"))
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
73
|
+
# Detailed results using tree structure
|
|
74
|
+
for idx, result in enumerate(report.results, 1):
|
|
75
|
+
self._format_policy_result_modern(console, result, idx, len(report.results))
|
|
76
|
+
|
|
77
|
+
# Final status with styled box
|
|
78
|
+
console.print()
|
|
79
|
+
self._print_final_status(console, report)
|
|
80
|
+
|
|
81
|
+
# Get the formatted output
|
|
82
|
+
output = string_buffer.getvalue()
|
|
83
|
+
string_buffer.close()
|
|
84
|
+
|
|
85
|
+
return output
|
|
86
|
+
|
|
87
|
+
def _print_summary_panel(self, console: Console, report: ValidationReport) -> None:
|
|
88
|
+
"""Print summary panel with clean metrics display."""
|
|
89
|
+
# Create a simple table for metrics without progress bars
|
|
90
|
+
metrics_table = Table.grid(padding=(0, 2))
|
|
91
|
+
metrics_table.add_column(style="bold", justify="left", width=30)
|
|
92
|
+
metrics_table.add_column(style="bold", justify="left", width=20)
|
|
93
|
+
|
|
94
|
+
# Total policies
|
|
95
|
+
metrics_table.add_row(
|
|
96
|
+
"📋 Total Policies",
|
|
97
|
+
str(report.total_policies),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Valid policies
|
|
101
|
+
if report.total_policies > 0:
|
|
102
|
+
valid_pct = report.valid_policies * 100 // report.total_policies
|
|
103
|
+
metrics_table.add_row(
|
|
104
|
+
"✅ Valid Policies",
|
|
105
|
+
f"[green]{report.valid_policies} ({valid_pct}%)[/green]",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Invalid policies
|
|
109
|
+
invalid_pct = report.invalid_policies * 100 // report.total_policies
|
|
110
|
+
if report.invalid_policies > 0:
|
|
111
|
+
metrics_table.add_row(
|
|
112
|
+
"❌ Invalid Policies",
|
|
113
|
+
f"[red]{report.invalid_policies} ({invalid_pct}%)[/red]",
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
metrics_table.add_row(
|
|
117
|
+
"❌ Invalid Policies",
|
|
118
|
+
f"[dim]{report.invalid_policies} ({invalid_pct}%)[/dim]",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Total issues
|
|
122
|
+
if report.total_issues > 0:
|
|
123
|
+
metrics_table.add_row(
|
|
124
|
+
"⚠️ Total Issues Found",
|
|
125
|
+
f"[red]{report.total_issues}[/red]",
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
metrics_table.add_row(
|
|
129
|
+
"⚠️ Total Issues Found",
|
|
130
|
+
f"[green]{report.total_issues}[/green]",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
console.print(
|
|
134
|
+
Panel(
|
|
135
|
+
metrics_table,
|
|
136
|
+
title="📊 Executive Summary",
|
|
137
|
+
border_style="bright_blue",
|
|
138
|
+
padding=(1, 2),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _create_progress_bar(self, value: int, total: int, color: str) -> str:
|
|
143
|
+
"""Create a simple text-based progress bar."""
|
|
144
|
+
if total == 0:
|
|
145
|
+
return "[dim]───────────────────────[/dim]"
|
|
146
|
+
|
|
147
|
+
percentage = min(value * 100 // total, 100)
|
|
148
|
+
filled = int(percentage / 5) # 20 bars total (100/5)
|
|
149
|
+
empty = 20 - filled
|
|
150
|
+
|
|
151
|
+
bar = f"[{color}]{'█' * filled}[/{color}][dim]{'░' * empty}[/dim]"
|
|
152
|
+
return bar
|
|
153
|
+
|
|
154
|
+
def _print_severity_breakdown(self, console: Console, report: ValidationReport) -> None:
|
|
155
|
+
"""Print a clean breakdown of issues by severity."""
|
|
156
|
+
# Count issues by severity
|
|
157
|
+
severity_counts = {
|
|
158
|
+
"critical": 0,
|
|
159
|
+
"high": 0,
|
|
160
|
+
"error": 0,
|
|
161
|
+
"medium": 0,
|
|
162
|
+
"warning": 0,
|
|
163
|
+
"low": 0,
|
|
164
|
+
"info": 0,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for result in report.results:
|
|
168
|
+
for issue in result.issues:
|
|
169
|
+
severity = issue.severity.lower()
|
|
170
|
+
if severity in severity_counts:
|
|
171
|
+
severity_counts[severity] += 1
|
|
172
|
+
|
|
173
|
+
# Create clean severity table
|
|
174
|
+
severity_table = Table.grid(padding=(0, 2))
|
|
175
|
+
severity_table.add_column(style="bold", justify="left", width=25)
|
|
176
|
+
severity_table.add_column(style="bold", justify="left", width=15)
|
|
177
|
+
|
|
178
|
+
# Show individual severity counts
|
|
179
|
+
if severity_counts["critical"] > 0:
|
|
180
|
+
severity_table.add_row(
|
|
181
|
+
"🔴 Critical",
|
|
182
|
+
f"[red]{severity_counts['critical']}[/red]",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if severity_counts["high"] > 0:
|
|
186
|
+
severity_table.add_row(
|
|
187
|
+
"🔴 High",
|
|
188
|
+
f"[red]{severity_counts['high']}[/red]",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if severity_counts["error"] > 0:
|
|
192
|
+
severity_table.add_row(
|
|
193
|
+
"🔴 Error",
|
|
194
|
+
f"[red]{severity_counts['error']}[/red]",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if severity_counts["medium"] > 0:
|
|
198
|
+
severity_table.add_row(
|
|
199
|
+
"🟡 Medium",
|
|
200
|
+
f"[yellow]{severity_counts['medium']}[/yellow]",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if severity_counts["warning"] > 0:
|
|
204
|
+
severity_table.add_row(
|
|
205
|
+
"🟡 Warning",
|
|
206
|
+
f"[yellow]{severity_counts['warning']}[/yellow]",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if severity_counts["low"] > 0:
|
|
210
|
+
severity_table.add_row(
|
|
211
|
+
"🔵 Low",
|
|
212
|
+
f"[blue]{severity_counts['low']}[/blue]",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if severity_counts["info"] > 0:
|
|
216
|
+
severity_table.add_row(
|
|
217
|
+
"🔵 Info",
|
|
218
|
+
f"[blue]{severity_counts['info']}[/blue]",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
console.print(
|
|
222
|
+
Panel(severity_table, title="🎯 Issue Severity Breakdown", border_style="bright_blue")
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _format_policy_result_modern(
|
|
226
|
+
self, console: Console, result: PolicyValidationResult, idx: int, total: int
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Format policy results with modern tree structure.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
console: Rich console instance
|
|
232
|
+
result: Policy validation result
|
|
233
|
+
idx: Index of this policy (1-based)
|
|
234
|
+
total: Total number of policies
|
|
235
|
+
"""
|
|
236
|
+
# Status icon and color
|
|
237
|
+
if result.is_valid and not result.issues:
|
|
238
|
+
icon = "✅"
|
|
239
|
+
color = "green"
|
|
240
|
+
status_text = "VALID"
|
|
241
|
+
elif result.is_valid and result.issues:
|
|
242
|
+
# Valid IAM policy but has security findings
|
|
243
|
+
# Check severity to determine the appropriate status
|
|
244
|
+
has_critical = any(i.severity in ("error", "critical", "high") for i in result.issues)
|
|
245
|
+
if has_critical:
|
|
246
|
+
icon = "⚠️"
|
|
247
|
+
color = "red"
|
|
248
|
+
status_text = "VALID (with security issues)"
|
|
249
|
+
else:
|
|
250
|
+
icon = "⚠️"
|
|
251
|
+
color = "yellow"
|
|
252
|
+
status_text = "VALID (with warnings)"
|
|
253
|
+
else:
|
|
254
|
+
# Policy failed validation (is_valid=false)
|
|
255
|
+
# Check if it's due to IAM errors or security issues
|
|
256
|
+
has_iam_errors = any(i.severity == "error" for i in result.issues)
|
|
257
|
+
has_security_critical = any(i.severity in ("critical", "high") for i in result.issues)
|
|
258
|
+
|
|
259
|
+
if has_iam_errors and has_security_critical:
|
|
260
|
+
# Both IAM errors and security issues
|
|
261
|
+
status_text = "INVALID (IAM errors + security issues)"
|
|
262
|
+
elif has_iam_errors:
|
|
263
|
+
# Only IAM validation errors
|
|
264
|
+
status_text = "INVALID (IAM errors)"
|
|
265
|
+
else:
|
|
266
|
+
# Only security issues (failed due to fail_on_severity config)
|
|
267
|
+
status_text = "FAILED (critical security issues)"
|
|
268
|
+
|
|
269
|
+
icon = "❌"
|
|
270
|
+
color = "red"
|
|
271
|
+
|
|
272
|
+
# Policy header
|
|
273
|
+
header = Text()
|
|
274
|
+
header.append(f"{icon} ", style=color)
|
|
275
|
+
header.append(f"[{idx}/{total}] ", style="dim")
|
|
276
|
+
header.append(result.policy_file, style=f"bold {color}")
|
|
277
|
+
header.append(f" • {status_text}", style=f"{color}")
|
|
278
|
+
|
|
279
|
+
if not result.issues:
|
|
280
|
+
console.print(header)
|
|
281
|
+
console.print(" [dim italic]No issues detected[/dim italic]")
|
|
282
|
+
console.print()
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
console.print(header)
|
|
286
|
+
console.print(f" [dim]{len(result.issues)} issue(s) found[/dim]")
|
|
287
|
+
console.print()
|
|
288
|
+
|
|
289
|
+
# Create tree structure for issues
|
|
290
|
+
tree = Tree(f"[bold]Issues ({len(result.issues)})[/bold]", guide_style="bright_black")
|
|
291
|
+
|
|
292
|
+
# Group issues by severity with proper categorization
|
|
293
|
+
critical_issues = [i for i in result.issues if i.severity == "critical"]
|
|
294
|
+
high_issues = [i for i in result.issues if i.severity == "high"]
|
|
295
|
+
error_issues = [i for i in result.issues if i.severity == "error"]
|
|
296
|
+
medium_issues = [i for i in result.issues if i.severity == "medium"]
|
|
297
|
+
warning_issues = [i for i in result.issues if i.severity == "warning"]
|
|
298
|
+
low_issues = [i for i in result.issues if i.severity == "low"]
|
|
299
|
+
info_issues = [i for i in result.issues if i.severity == "info"]
|
|
300
|
+
|
|
301
|
+
# Add critical issues (security checks)
|
|
302
|
+
if critical_issues:
|
|
303
|
+
critical_branch = tree.add("🔴 [bold red]Critical[/bold red]")
|
|
304
|
+
for issue in critical_issues:
|
|
305
|
+
self._add_issue_to_tree(critical_branch, issue, "red")
|
|
306
|
+
|
|
307
|
+
# Add high severity issues (security checks)
|
|
308
|
+
if high_issues:
|
|
309
|
+
high_branch = tree.add("🔴 [bold red]High[/bold red]")
|
|
310
|
+
for issue in high_issues:
|
|
311
|
+
self._add_issue_to_tree(high_branch, issue, "red")
|
|
312
|
+
|
|
313
|
+
# Add errors (IAM validation)
|
|
314
|
+
if error_issues:
|
|
315
|
+
error_branch = tree.add("🔴 [bold red]Error[/bold red]")
|
|
316
|
+
for issue in error_issues:
|
|
317
|
+
self._add_issue_to_tree(error_branch, issue, "red")
|
|
318
|
+
|
|
319
|
+
# Add medium severity issues (security checks)
|
|
320
|
+
if medium_issues:
|
|
321
|
+
medium_branch = tree.add("🟡 [bold yellow]Medium[/bold yellow]")
|
|
322
|
+
for issue in medium_issues:
|
|
323
|
+
self._add_issue_to_tree(medium_branch, issue, "yellow")
|
|
324
|
+
|
|
325
|
+
# Add warnings (IAM validation)
|
|
326
|
+
if warning_issues:
|
|
327
|
+
warning_branch = tree.add("🟡 [bold yellow]Warning[/bold yellow]")
|
|
328
|
+
for issue in warning_issues:
|
|
329
|
+
self._add_issue_to_tree(warning_branch, issue, "yellow")
|
|
330
|
+
|
|
331
|
+
# Add low severity issues (security checks)
|
|
332
|
+
if low_issues:
|
|
333
|
+
low_branch = tree.add("🔵 [bold blue]Low[/bold blue]")
|
|
334
|
+
for issue in low_issues:
|
|
335
|
+
self._add_issue_to_tree(low_branch, issue, "blue")
|
|
336
|
+
|
|
337
|
+
# Add info (IAM validation)
|
|
338
|
+
if info_issues:
|
|
339
|
+
info_branch = tree.add("🔵 [bold blue]Info[/bold blue]")
|
|
340
|
+
for issue in info_issues:
|
|
341
|
+
self._add_issue_to_tree(info_branch, issue, "blue")
|
|
342
|
+
|
|
343
|
+
console.print(" ", tree)
|
|
344
|
+
console.print()
|
|
345
|
+
|
|
346
|
+
def _add_issue_to_tree(self, branch: Tree, issue, color: str) -> None:
|
|
347
|
+
"""Add an issue to a tree branch."""
|
|
348
|
+
# Build location string
|
|
349
|
+
location = f"Statement {issue.statement_index}"
|
|
350
|
+
if issue.statement_sid:
|
|
351
|
+
location = f"{issue.statement_sid} (#{issue.statement_index})"
|
|
352
|
+
if issue.line_number is not None:
|
|
353
|
+
location += f" @L{issue.line_number}"
|
|
354
|
+
|
|
355
|
+
# Issue summary
|
|
356
|
+
issue_text = Text()
|
|
357
|
+
issue_text.append(f"[{location}] ", style="dim")
|
|
358
|
+
issue_text.append(issue.issue_type, style=f"bold {color}")
|
|
359
|
+
issue_node = branch.add(issue_text)
|
|
360
|
+
|
|
361
|
+
# Message
|
|
362
|
+
msg_node = issue_node.add(Text(issue.message, style="white"))
|
|
363
|
+
|
|
364
|
+
# Details
|
|
365
|
+
if issue.action or issue.resource or issue.condition_key:
|
|
366
|
+
details = []
|
|
367
|
+
if issue.action:
|
|
368
|
+
details.append(f"Action: {issue.action}")
|
|
369
|
+
if issue.resource:
|
|
370
|
+
details.append(f"Resource: {issue.resource}")
|
|
371
|
+
if issue.condition_key:
|
|
372
|
+
details.append(f"Condition: {issue.condition_key}")
|
|
373
|
+
msg_node.add(Text(" • ".join(details), style="dim cyan"))
|
|
374
|
+
|
|
375
|
+
# Suggestion
|
|
376
|
+
if issue.suggestion:
|
|
377
|
+
suggestion_text = Text()
|
|
378
|
+
suggestion_text.append("💡 ", style="yellow")
|
|
379
|
+
suggestion_text.append(issue.suggestion, style="italic yellow")
|
|
380
|
+
msg_node.add(suggestion_text)
|
|
381
|
+
|
|
382
|
+
def _print_final_status(self, console: Console, report: ValidationReport) -> None:
|
|
383
|
+
"""Print final status panel."""
|
|
384
|
+
if report.invalid_policies == 0 and report.total_issues == 0:
|
|
385
|
+
# Perfect success
|
|
386
|
+
status = Text("🎉 ALL POLICIES VALIDATED SUCCESSFULLY! 🎉", style="bold green")
|
|
387
|
+
message = Text(
|
|
388
|
+
f"All {report.valid_policies} policies passed validation with no issues.",
|
|
389
|
+
style="green",
|
|
390
|
+
)
|
|
391
|
+
border_color = "green"
|
|
392
|
+
elif report.invalid_policies == 0:
|
|
393
|
+
# Valid IAM policies but may have security findings
|
|
394
|
+
# Check if there are critical/high security issues
|
|
395
|
+
has_critical = any(
|
|
396
|
+
i.severity in ("error", "critical", "high")
|
|
397
|
+
for r in report.results
|
|
398
|
+
for i in r.issues
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if has_critical:
|
|
402
|
+
status = Text("⚠️ All Policies Valid (with security issues)", style="bold red")
|
|
403
|
+
message = Text(
|
|
404
|
+
f"{report.valid_policies} policies are valid, but {report.total_issues} "
|
|
405
|
+
f"security issue(s) were found that must be addressed.",
|
|
406
|
+
style="red",
|
|
407
|
+
)
|
|
408
|
+
border_color = "red"
|
|
409
|
+
else:
|
|
410
|
+
status = Text("✅ All Policies Valid (with warnings)", style="bold yellow")
|
|
411
|
+
message = Text(
|
|
412
|
+
f"{report.valid_policies} policies are valid, but {report.total_issues} "
|
|
413
|
+
f"warning(s) were found that should be reviewed.",
|
|
414
|
+
style="yellow",
|
|
415
|
+
)
|
|
416
|
+
border_color = "yellow"
|
|
417
|
+
else:
|
|
418
|
+
# Has invalid policies
|
|
419
|
+
status = Text("❌ VALIDATION FAILED", style="bold red")
|
|
420
|
+
message = Text(
|
|
421
|
+
f"{report.invalid_policies} of {report.total_policies} policies have critical "
|
|
422
|
+
f"issues that must be resolved.",
|
|
423
|
+
style="red",
|
|
424
|
+
)
|
|
425
|
+
border_color = "red"
|
|
426
|
+
|
|
427
|
+
# Combine status and message
|
|
428
|
+
final_text = Text()
|
|
429
|
+
final_text.append(status)
|
|
430
|
+
final_text.append("\n\n")
|
|
431
|
+
final_text.append(message)
|
|
432
|
+
|
|
433
|
+
console.print(Panel(final_text, border_style=border_color, padding=(1, 2)))
|
|
@@ -228,21 +228,43 @@ class HTMLFormatter(OutputFormatter):
|
|
|
228
228
|
text-transform: uppercase;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/* IAM Validity Severities */
|
|
231
232
|
.severity-error {
|
|
232
|
-
background:
|
|
233
|
+
background: #dc3545;
|
|
233
234
|
color: white;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
.severity-warning {
|
|
237
|
-
background:
|
|
238
|
+
background: #ffc107;
|
|
238
239
|
color: #333;
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
.severity-info {
|
|
242
|
-
background:
|
|
243
|
+
background: #0dcaf0;
|
|
243
244
|
color: white;
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/* Security Severities */
|
|
248
|
+
.severity-critical {
|
|
249
|
+
background: #8b0000;
|
|
250
|
+
color: white;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.severity-high {
|
|
254
|
+
background: #ff6b6b;
|
|
255
|
+
color: white;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.severity-medium {
|
|
259
|
+
background: #ffa500;
|
|
260
|
+
color: #333;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.severity-low {
|
|
264
|
+
background: #90caf9;
|
|
265
|
+
color: #333;
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
.chart-container {
|
|
247
269
|
position: relative;
|
|
248
270
|
height: 300px;
|
|
@@ -296,19 +318,6 @@ class HTMLFormatter(OutputFormatter):
|
|
|
296
318
|
|
|
297
319
|
def _render_summary(self, report: ValidationReport, include_charts: bool) -> str:
|
|
298
320
|
"""Render summary section with statistics."""
|
|
299
|
-
# Count issues by severity - support both IAM validity and security severities
|
|
300
|
-
error_count = sum(
|
|
301
|
-
1
|
|
302
|
-
for r in report.results
|
|
303
|
-
for i in r.issues
|
|
304
|
-
if i.severity in ("error", "critical", "high")
|
|
305
|
-
)
|
|
306
|
-
warning_count = sum(
|
|
307
|
-
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
308
|
-
)
|
|
309
|
-
info_count = sum(
|
|
310
|
-
1 for r in report.results for i in r.issues if i.severity in ("info", "low")
|
|
311
|
-
)
|
|
312
321
|
total_issues = report.total_issues
|
|
313
322
|
|
|
314
323
|
html_parts = [
|
|
@@ -321,24 +330,28 @@ class HTMLFormatter(OutputFormatter):
|
|
|
321
330
|
<div class="stat-label">Total Policies</div>
|
|
322
331
|
</div>
|
|
323
332
|
<div class="stat-card">
|
|
324
|
-
<div class="stat-value">{report.valid_policies}</div>
|
|
325
|
-
<div class="stat-label">Valid
|
|
333
|
+
<div class="stat-value" style="color: var(--success-color)">{report.valid_policies}</div>
|
|
334
|
+
<div class="stat-label">Valid (IAM)</div>
|
|
326
335
|
</div>
|
|
327
336
|
<div class="stat-card">
|
|
328
|
-
<div class="stat-value">{
|
|
329
|
-
<div class="stat-label">
|
|
337
|
+
<div class="stat-value" style="color: var(--error-color)">{report.invalid_policies}</div>
|
|
338
|
+
<div class="stat-label">Invalid (IAM)</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="stat-card">
|
|
341
|
+
<div class="stat-value" style="color: var(--warning-color)">{report.policies_with_security_issues}</div>
|
|
342
|
+
<div class="stat-label">Security Findings</div>
|
|
330
343
|
</div>
|
|
331
344
|
<div class="stat-card">
|
|
332
|
-
<div class="stat-value"
|
|
333
|
-
<div class="stat-label">
|
|
345
|
+
<div class="stat-value">{total_issues}</div>
|
|
346
|
+
<div class="stat-label">Total Issues</div>
|
|
334
347
|
</div>
|
|
335
348
|
<div class="stat-card">
|
|
336
|
-
<div class="stat-value" style="color: var(--
|
|
337
|
-
<div class="stat-label">
|
|
349
|
+
<div class="stat-value" style="color: var(--error-color)">{report.validity_issues}</div>
|
|
350
|
+
<div class="stat-label">Validity Issues</div>
|
|
338
351
|
</div>
|
|
339
352
|
<div class="stat-card">
|
|
340
|
-
<div class="stat-value" style="color: var(--
|
|
341
|
-
<div class="stat-label">
|
|
353
|
+
<div class="stat-value" style="color: var(--warning-color)">{report.security_issues}</div>
|
|
354
|
+
<div class="stat-label">Security Issues</div>
|
|
342
355
|
</div>
|
|
343
356
|
</div>
|
|
344
357
|
"""
|
|
@@ -370,9 +383,17 @@ class HTMLFormatter(OutputFormatter):
|
|
|
370
383
|
<label for="severityFilter">Severity</label>
|
|
371
384
|
<select id="severityFilter">
|
|
372
385
|
<option value="">All</option>
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
386
|
+
<optgroup label="IAM Validity">
|
|
387
|
+
<option value="error">Error</option>
|
|
388
|
+
<option value="warning">Warning</option>
|
|
389
|
+
<option value="info">Info</option>
|
|
390
|
+
</optgroup>
|
|
391
|
+
<optgroup label="Security">
|
|
392
|
+
<option value="critical">Critical</option>
|
|
393
|
+
<option value="high">High</option>
|
|
394
|
+
<option value="medium">Medium</option>
|
|
395
|
+
<option value="low">Low</option>
|
|
396
|
+
</optgroup>
|
|
376
397
|
</select>
|
|
377
398
|
</div>
|
|
378
399
|
<div class="filter-group">
|
|
@@ -391,11 +412,38 @@ class HTMLFormatter(OutputFormatter):
|
|
|
391
412
|
</section>
|
|
392
413
|
"""
|
|
393
414
|
|
|
415
|
+
def _format_suggestion(self, suggestion: str) -> str:
|
|
416
|
+
"""Format suggestion field to show examples in code blocks."""
|
|
417
|
+
if not suggestion:
|
|
418
|
+
return "-"
|
|
419
|
+
|
|
420
|
+
# Check if suggestion contains "Example:" section
|
|
421
|
+
if "\nExample:\n" in suggestion:
|
|
422
|
+
parts = suggestion.split("\nExample:\n", 1)
|
|
423
|
+
text_part = html.escape(parts[0])
|
|
424
|
+
code_part = html.escape(parts[1])
|
|
425
|
+
|
|
426
|
+
return f"""
|
|
427
|
+
<div>
|
|
428
|
+
<div>{text_part}</div>
|
|
429
|
+
<details style="margin-top: 10px;">
|
|
430
|
+
<summary style="cursor: pointer; font-weight: 500; color: var(--text-secondary);">
|
|
431
|
+
📖 View Example
|
|
432
|
+
</summary>
|
|
433
|
+
<pre class="code-block" style="margin-top: 10px; white-space: pre-wrap;">{code_part}</pre>
|
|
434
|
+
</details>
|
|
435
|
+
</div>
|
|
436
|
+
"""
|
|
437
|
+
else:
|
|
438
|
+
return html.escape(suggestion)
|
|
439
|
+
|
|
394
440
|
def _render_issues_table(self, report: ValidationReport) -> str:
|
|
395
441
|
"""Render issues table."""
|
|
396
442
|
rows = []
|
|
397
443
|
for policy_result in report.results:
|
|
398
444
|
for issue in policy_result.issues:
|
|
445
|
+
formatted_suggestion = self._format_suggestion(issue.suggestion)
|
|
446
|
+
|
|
399
447
|
row = f"""
|
|
400
448
|
<tr class="issue-row"
|
|
401
449
|
data-severity="{issue.severity}"
|
|
@@ -407,7 +455,7 @@ class HTMLFormatter(OutputFormatter):
|
|
|
407
455
|
<td><span class="severity-badge severity-{issue.severity}">{issue.severity}</span></td>
|
|
408
456
|
<td>{html.escape(issue.issue_type or "-")}</td>
|
|
409
457
|
<td>{html.escape(issue.message)}</td>
|
|
410
|
-
<td>{
|
|
458
|
+
<td>{formatted_suggestion}</td>
|
|
411
459
|
</tr>
|
|
412
460
|
"""
|
|
413
461
|
rows.append(row)
|
|
@@ -519,21 +567,63 @@ class HTMLFormatter(OutputFormatter):
|
|
|
519
567
|
if (typeof Chart !== 'undefined') {
|
|
520
568
|
const ctx = document.getElementById('severityChart');
|
|
521
569
|
if (ctx) {
|
|
570
|
+
// Count all severity types
|
|
571
|
+
const criticalCount = document.querySelectorAll('[data-severity="critical"]').length;
|
|
572
|
+
const highCount = document.querySelectorAll('[data-severity="high"]').length;
|
|
573
|
+
const mediumCount = document.querySelectorAll('[data-severity="medium"]').length;
|
|
574
|
+
const lowCount = document.querySelectorAll('[data-severity="low"]').length;
|
|
522
575
|
const errorCount = document.querySelectorAll('[data-severity="error"]').length;
|
|
523
576
|
const warningCount = document.querySelectorAll('[data-severity="warning"]').length;
|
|
524
577
|
const infoCount = document.querySelectorAll('[data-severity="info"]').length;
|
|
525
578
|
|
|
579
|
+
// Build labels and data arrays dynamically
|
|
580
|
+
const labels = [];
|
|
581
|
+
const data = [];
|
|
582
|
+
const colors = [];
|
|
583
|
+
|
|
584
|
+
if (criticalCount > 0) {
|
|
585
|
+
labels.push('Critical');
|
|
586
|
+
data.push(criticalCount);
|
|
587
|
+
colors.push('rgba(139, 0, 0, 0.8)');
|
|
588
|
+
}
|
|
589
|
+
if (highCount > 0) {
|
|
590
|
+
labels.push('High');
|
|
591
|
+
data.push(highCount);
|
|
592
|
+
colors.push('rgba(255, 107, 107, 0.8)');
|
|
593
|
+
}
|
|
594
|
+
if (errorCount > 0) {
|
|
595
|
+
labels.push('Error');
|
|
596
|
+
data.push(errorCount);
|
|
597
|
+
colors.push('rgba(220, 53, 69, 0.8)');
|
|
598
|
+
}
|
|
599
|
+
if (mediumCount > 0) {
|
|
600
|
+
labels.push('Medium');
|
|
601
|
+
data.push(mediumCount);
|
|
602
|
+
colors.push('rgba(255, 165, 0, 0.8)');
|
|
603
|
+
}
|
|
604
|
+
if (warningCount > 0) {
|
|
605
|
+
labels.push('Warning');
|
|
606
|
+
data.push(warningCount);
|
|
607
|
+
colors.push('rgba(255, 193, 7, 0.8)');
|
|
608
|
+
}
|
|
609
|
+
if (infoCount > 0) {
|
|
610
|
+
labels.push('Info');
|
|
611
|
+
data.push(infoCount);
|
|
612
|
+
colors.push('rgba(13, 202, 240, 0.8)');
|
|
613
|
+
}
|
|
614
|
+
if (lowCount > 0) {
|
|
615
|
+
labels.push('Low');
|
|
616
|
+
data.push(lowCount);
|
|
617
|
+
colors.push('rgba(144, 202, 249, 0.8)');
|
|
618
|
+
}
|
|
619
|
+
|
|
526
620
|
new Chart(ctx, {
|
|
527
621
|
type: 'doughnut',
|
|
528
622
|
data: {
|
|
529
|
-
labels:
|
|
623
|
+
labels: labels,
|
|
530
624
|
datasets: [{
|
|
531
|
-
data:
|
|
532
|
-
backgroundColor:
|
|
533
|
-
'rgba(220, 53, 69, 0.8)',
|
|
534
|
-
'rgba(255, 193, 7, 0.8)',
|
|
535
|
-
'rgba(13, 202, 240, 0.8)'
|
|
536
|
-
]
|
|
625
|
+
data: data,
|
|
626
|
+
backgroundColor: colors
|
|
537
627
|
}]
|
|
538
628
|
},
|
|
539
629
|
options: {
|