iam-policy-validator 1.7.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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- 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 +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/config/config_loader.py +472 -0
- iam_validator/core/config/defaults.py +523 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/constants.py +74 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +440 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""HTML formatter for IAM Policy Validator with interactive features."""
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
7
|
+
from iam_validator.core.models import ValidationReport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HTMLFormatter(OutputFormatter):
|
|
11
|
+
"""Formats validation results as interactive HTML report."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def format_id(self) -> str:
|
|
15
|
+
return "html"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def description(self) -> str:
|
|
19
|
+
return "Interactive HTML report with filtering and search"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def file_extension(self) -> str:
|
|
23
|
+
return "html"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def content_type(self) -> str:
|
|
27
|
+
return "text/html"
|
|
28
|
+
|
|
29
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
30
|
+
"""Format report as HTML.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
report: The validation report
|
|
34
|
+
**kwargs: Additional options like 'title', 'include_charts'
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
HTML string
|
|
38
|
+
"""
|
|
39
|
+
title = kwargs.get("title", "IAM Policy Validation Report")
|
|
40
|
+
include_charts = kwargs.get("include_charts", True)
|
|
41
|
+
dark_mode = kwargs.get("dark_mode", False)
|
|
42
|
+
|
|
43
|
+
html_content = f"""<!DOCTYPE html>
|
|
44
|
+
<html lang="en" class="{" dark" if dark_mode else ""}">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8">
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
48
|
+
<title>{html.escape(title)}</title>
|
|
49
|
+
{self._get_styles(dark_mode)}
|
|
50
|
+
{self._get_scripts(include_charts)}
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="container">
|
|
54
|
+
<header>
|
|
55
|
+
<h1>🛡️ {html.escape(title)}</h1>
|
|
56
|
+
<div class="timestamp">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
{self._render_summary(report, include_charts)}
|
|
60
|
+
{self._render_filters()}
|
|
61
|
+
{self._render_issues_table(report)}
|
|
62
|
+
{self._render_policy_details(report)}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{self._get_javascript()}
|
|
66
|
+
</body>
|
|
67
|
+
</html>"""
|
|
68
|
+
|
|
69
|
+
return html_content
|
|
70
|
+
|
|
71
|
+
def _get_styles(self, dark_mode: bool) -> str:
|
|
72
|
+
"""Get CSS styles for the report."""
|
|
73
|
+
return """
|
|
74
|
+
<style>
|
|
75
|
+
:root {
|
|
76
|
+
--bg-primary: #ffffff;
|
|
77
|
+
--bg-secondary: #f8f9fa;
|
|
78
|
+
--text-primary: #212529;
|
|
79
|
+
--text-secondary: #6c757d;
|
|
80
|
+
--border-color: #dee2e6;
|
|
81
|
+
--error-color: #dc3545;
|
|
82
|
+
--warning-color: #ffc107;
|
|
83
|
+
--info-color: #0dcaf0;
|
|
84
|
+
--success-color: #198754;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dark {
|
|
88
|
+
--bg-primary: #1a1a1a;
|
|
89
|
+
--bg-secondary: #2d2d2d;
|
|
90
|
+
--text-primary: #e0e0e0;
|
|
91
|
+
--text-secondary: #a0a0a0;
|
|
92
|
+
--border-color: #404040;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
* {
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding: 0;
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
body {
|
|
102
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
103
|
+
background-color: var(--bg-primary);
|
|
104
|
+
color: var(--text-primary);
|
|
105
|
+
line-height: 1.6;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.container {
|
|
109
|
+
max-width: 1400px;
|
|
110
|
+
margin: 0 auto;
|
|
111
|
+
padding: 20px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
header {
|
|
115
|
+
border-bottom: 2px solid var(--border-color);
|
|
116
|
+
padding-bottom: 20px;
|
|
117
|
+
margin-bottom: 30px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
h1 {
|
|
121
|
+
font-size: 2.5rem;
|
|
122
|
+
margin-bottom: 10px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.timestamp {
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
font-size: 0.9rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.summary-grid {
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
133
|
+
gap: 20px;
|
|
134
|
+
margin-bottom: 30px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.stat-card {
|
|
138
|
+
background: var(--bg-secondary);
|
|
139
|
+
border-radius: 8px;
|
|
140
|
+
padding: 20px;
|
|
141
|
+
border: 1px solid var(--border-color);
|
|
142
|
+
transition: transform 0.2s;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.stat-card:hover {
|
|
146
|
+
transform: translateY(-2px);
|
|
147
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.stat-value {
|
|
151
|
+
font-size: 2rem;
|
|
152
|
+
font-weight: bold;
|
|
153
|
+
margin-bottom: 5px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.stat-label {
|
|
157
|
+
color: var(--text-secondary);
|
|
158
|
+
font-size: 0.9rem;
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.filters {
|
|
163
|
+
background: var(--bg-secondary);
|
|
164
|
+
border-radius: 8px;
|
|
165
|
+
padding: 20px;
|
|
166
|
+
margin-bottom: 30px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.filter-row {
|
|
170
|
+
display: flex;
|
|
171
|
+
gap: 15px;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
align-items: center;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.filter-group {
|
|
177
|
+
flex: 1;
|
|
178
|
+
min-width: 200px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.filter-group label {
|
|
182
|
+
display: block;
|
|
183
|
+
margin-bottom: 5px;
|
|
184
|
+
font-weight: 500;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.filter-group input,
|
|
188
|
+
.filter-group select {
|
|
189
|
+
width: 100%;
|
|
190
|
+
padding: 8px 12px;
|
|
191
|
+
border: 1px solid var(--border-color);
|
|
192
|
+
border-radius: 4px;
|
|
193
|
+
background: var(--bg-primary);
|
|
194
|
+
color: var(--text-primary);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
table {
|
|
198
|
+
width: 100%;
|
|
199
|
+
border-collapse: collapse;
|
|
200
|
+
background: var(--bg-secondary);
|
|
201
|
+
border-radius: 8px;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
th {
|
|
206
|
+
background: var(--bg-primary);
|
|
207
|
+
padding: 12px;
|
|
208
|
+
text-align: left;
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
border-bottom: 2px solid var(--border-color);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
td {
|
|
214
|
+
padding: 12px;
|
|
215
|
+
border-bottom: 1px solid var(--border-color);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
tr:hover {
|
|
219
|
+
background: var(--bg-primary);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.severity-badge {
|
|
223
|
+
display: inline-block;
|
|
224
|
+
padding: 4px 8px;
|
|
225
|
+
border-radius: 4px;
|
|
226
|
+
font-size: 0.85rem;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* IAM Validity Severities */
|
|
232
|
+
.severity-error {
|
|
233
|
+
background: #dc3545;
|
|
234
|
+
color: white;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.severity-warning {
|
|
238
|
+
background: #ffc107;
|
|
239
|
+
color: #333;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.severity-info {
|
|
243
|
+
background: #0dcaf0;
|
|
244
|
+
color: white;
|
|
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
|
+
|
|
268
|
+
.chart-container {
|
|
269
|
+
position: relative;
|
|
270
|
+
height: 300px;
|
|
271
|
+
margin: 20px 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.hidden {
|
|
275
|
+
display: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.expandable {
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.expandable:hover {
|
|
283
|
+
text-decoration: underline;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.details-panel {
|
|
287
|
+
background: var(--bg-primary);
|
|
288
|
+
border: 1px solid var(--border-color);
|
|
289
|
+
border-radius: 4px;
|
|
290
|
+
padding: 15px;
|
|
291
|
+
margin-top: 10px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.code-block {
|
|
295
|
+
background: #f5f5f5;
|
|
296
|
+
border: 1px solid var(--border-color);
|
|
297
|
+
border-radius: 4px;
|
|
298
|
+
padding: 10px;
|
|
299
|
+
font-family: 'Courier New', monospace;
|
|
300
|
+
font-size: 0.9rem;
|
|
301
|
+
overflow-x: auto;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.dark .code-block {
|
|
305
|
+
background: #1e1e1e;
|
|
306
|
+
}
|
|
307
|
+
</style>
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def _get_scripts(self, include_charts: bool) -> str:
|
|
311
|
+
"""Get JavaScript dependencies."""
|
|
312
|
+
scripts = ""
|
|
313
|
+
if include_charts:
|
|
314
|
+
scripts += """
|
|
315
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
316
|
+
"""
|
|
317
|
+
return scripts
|
|
318
|
+
|
|
319
|
+
def _render_summary(self, report: ValidationReport, include_charts: bool) -> str:
|
|
320
|
+
"""Render summary section with statistics."""
|
|
321
|
+
total_issues = report.total_issues
|
|
322
|
+
|
|
323
|
+
html_parts = [
|
|
324
|
+
f"""
|
|
325
|
+
<section class="summary">
|
|
326
|
+
<h2>Summary</h2>
|
|
327
|
+
<div class="summary-grid">
|
|
328
|
+
<div class="stat-card">
|
|
329
|
+
<div class="stat-value">{report.total_policies}</div>
|
|
330
|
+
<div class="stat-label">Total Policies</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="stat-card">
|
|
333
|
+
<div class="stat-value" style="color: var(--success-color)">{report.valid_policies}</div>
|
|
334
|
+
<div class="stat-label">Valid (IAM)</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="stat-card">
|
|
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>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="stat-card">
|
|
345
|
+
<div class="stat-value">{total_issues}</div>
|
|
346
|
+
<div class="stat-label">Total Issues</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="stat-card">
|
|
349
|
+
<div class="stat-value" style="color: var(--error-color)">{report.validity_issues}</div>
|
|
350
|
+
<div class="stat-label">Validity Issues</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="stat-card">
|
|
353
|
+
<div class="stat-value" style="color: var(--warning-color)">{report.security_issues}</div>
|
|
354
|
+
<div class="stat-label">Security Issues</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
"""
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
if include_charts and total_issues > 0:
|
|
361
|
+
html_parts.append(
|
|
362
|
+
"""
|
|
363
|
+
<div class="chart-container">
|
|
364
|
+
<canvas id="severityChart"></canvas>
|
|
365
|
+
</div>
|
|
366
|
+
"""
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
html_parts.append("</section>")
|
|
370
|
+
return "".join(html_parts)
|
|
371
|
+
|
|
372
|
+
def _render_filters(self) -> str:
|
|
373
|
+
"""Render filter controls."""
|
|
374
|
+
return """
|
|
375
|
+
<section class="filters">
|
|
376
|
+
<h2>Filters</h2>
|
|
377
|
+
<div class="filter-row">
|
|
378
|
+
<div class="filter-group">
|
|
379
|
+
<label for="searchInput">Search</label>
|
|
380
|
+
<input type="text" id="searchInput" placeholder="Search messages...">
|
|
381
|
+
</div>
|
|
382
|
+
<div class="filter-group">
|
|
383
|
+
<label for="severityFilter">Severity</label>
|
|
384
|
+
<select id="severityFilter">
|
|
385
|
+
<option value="">All</option>
|
|
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>
|
|
397
|
+
</select>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="filter-group">
|
|
400
|
+
<label for="fileFilter">Policy File</label>
|
|
401
|
+
<select id="fileFilter">
|
|
402
|
+
<option value="">All Files</option>
|
|
403
|
+
</select>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="filter-group">
|
|
406
|
+
<label for="checkFilter">Check Type</label>
|
|
407
|
+
<select id="checkFilter">
|
|
408
|
+
<option value="">All Checks</option>
|
|
409
|
+
</select>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</section>
|
|
413
|
+
"""
|
|
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
|
+
|
|
440
|
+
def _render_issues_table(self, report: ValidationReport) -> str:
|
|
441
|
+
"""Render issues table."""
|
|
442
|
+
rows = []
|
|
443
|
+
for policy_result in report.results:
|
|
444
|
+
for issue in policy_result.issues:
|
|
445
|
+
formatted_suggestion = self._format_suggestion(issue.suggestion)
|
|
446
|
+
|
|
447
|
+
row = f"""
|
|
448
|
+
<tr class="issue-row"
|
|
449
|
+
data-severity="{issue.severity}"
|
|
450
|
+
data-file="{html.escape(policy_result.policy_file)}"
|
|
451
|
+
data-check="{html.escape(issue.issue_type or "")}"
|
|
452
|
+
data-message="{html.escape(issue.message.lower())}">
|
|
453
|
+
<td>{html.escape(policy_result.policy_file)}</td>
|
|
454
|
+
<td>{issue.line_number or "-"}</td>
|
|
455
|
+
<td><span class="severity-badge severity-{issue.severity}">{issue.severity}</span></td>
|
|
456
|
+
<td>{html.escape(issue.issue_type or "-")}</td>
|
|
457
|
+
<td>{html.escape(issue.message)}</td>
|
|
458
|
+
<td>{formatted_suggestion}</td>
|
|
459
|
+
</tr>
|
|
460
|
+
"""
|
|
461
|
+
rows.append(row)
|
|
462
|
+
|
|
463
|
+
return f"""
|
|
464
|
+
<section class="issues">
|
|
465
|
+
<h2>Issues</h2>
|
|
466
|
+
<table id="issuesTable">
|
|
467
|
+
<thead>
|
|
468
|
+
<tr>
|
|
469
|
+
<th>Policy File</th>
|
|
470
|
+
<th>Line</th>
|
|
471
|
+
<th>Severity</th>
|
|
472
|
+
<th>Check</th>
|
|
473
|
+
<th>Message</th>
|
|
474
|
+
<th>Suggestion</th>
|
|
475
|
+
</tr>
|
|
476
|
+
</thead>
|
|
477
|
+
<tbody>
|
|
478
|
+
{"".join(rows) if rows else '<tr><td colspan="6" style="text-align: center">No issues found</td></tr>'}
|
|
479
|
+
</tbody>
|
|
480
|
+
</table>
|
|
481
|
+
</section>
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def _render_policy_details(self, report: ValidationReport) -> str:
|
|
485
|
+
"""Render detailed policy information."""
|
|
486
|
+
details = []
|
|
487
|
+
for policy_result in report.results:
|
|
488
|
+
if not policy_result.issues:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
issues_by_statement = {}
|
|
492
|
+
for issue in policy_result.issues:
|
|
493
|
+
stmt_idx = issue.statement_index or -1
|
|
494
|
+
if stmt_idx not in issues_by_statement:
|
|
495
|
+
issues_by_statement[stmt_idx] = []
|
|
496
|
+
issues_by_statement[stmt_idx].append(issue)
|
|
497
|
+
|
|
498
|
+
detail = f"""
|
|
499
|
+
<div class="policy-detail" data-file="{html.escape(policy_result.policy_file)}">
|
|
500
|
+
<h3>{html.escape(policy_result.policy_file)}</h3>
|
|
501
|
+
<p>Total Issues: {len(policy_result.issues)}</p>
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
for stmt_idx, issues in sorted(issues_by_statement.items()):
|
|
505
|
+
detail += f"""
|
|
506
|
+
<div class="statement-issues">
|
|
507
|
+
<h4>Statement {stmt_idx + 1 if stmt_idx >= 0 else "Global"}</h4>
|
|
508
|
+
<ul>
|
|
509
|
+
"""
|
|
510
|
+
for issue in issues:
|
|
511
|
+
detail += f"""
|
|
512
|
+
<li>
|
|
513
|
+
<span class="severity-badge severity-{issue.severity}">{issue.severity}</span>
|
|
514
|
+
{html.escape(issue.message)}
|
|
515
|
+
{f"<br><em>{html.escape(issue.suggestion)}</em>" if issue.suggestion else ""}
|
|
516
|
+
</li>
|
|
517
|
+
"""
|
|
518
|
+
detail += """
|
|
519
|
+
</ul>
|
|
520
|
+
</div>
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
detail += "</div>"
|
|
524
|
+
details.append(detail)
|
|
525
|
+
|
|
526
|
+
return f"""
|
|
527
|
+
<section class="policy-details hidden">
|
|
528
|
+
<h2>Policy Details</h2>
|
|
529
|
+
{"".join(details)}
|
|
530
|
+
</section>
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
def _get_javascript(self) -> str:
|
|
534
|
+
"""Get JavaScript for interactivity."""
|
|
535
|
+
return """
|
|
536
|
+
<script>
|
|
537
|
+
// Populate filter dropdowns
|
|
538
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
539
|
+
const rows = document.querySelectorAll('.issue-row');
|
|
540
|
+
const files = new Set();
|
|
541
|
+
const checks = new Set();
|
|
542
|
+
|
|
543
|
+
rows.forEach(row => {
|
|
544
|
+
files.add(row.dataset.file);
|
|
545
|
+
checks.add(row.dataset.check);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const fileFilter = document.getElementById('fileFilter');
|
|
549
|
+
files.forEach(file => {
|
|
550
|
+
const option = document.createElement('option');
|
|
551
|
+
option.value = file;
|
|
552
|
+
option.textContent = file;
|
|
553
|
+
fileFilter.appendChild(option);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const checkFilter = document.getElementById('checkFilter');
|
|
557
|
+
checks.forEach(check => {
|
|
558
|
+
if (check) {
|
|
559
|
+
const option = document.createElement('option');
|
|
560
|
+
option.value = check;
|
|
561
|
+
option.textContent = check;
|
|
562
|
+
checkFilter.appendChild(option);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Draw severity chart if Chart.js is loaded
|
|
567
|
+
if (typeof Chart !== 'undefined') {
|
|
568
|
+
const ctx = document.getElementById('severityChart');
|
|
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;
|
|
575
|
+
const errorCount = document.querySelectorAll('[data-severity="error"]').length;
|
|
576
|
+
const warningCount = document.querySelectorAll('[data-severity="warning"]').length;
|
|
577
|
+
const infoCount = document.querySelectorAll('[data-severity="info"]').length;
|
|
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
|
+
|
|
620
|
+
new Chart(ctx, {
|
|
621
|
+
type: 'doughnut',
|
|
622
|
+
data: {
|
|
623
|
+
labels: labels,
|
|
624
|
+
datasets: [{
|
|
625
|
+
data: data,
|
|
626
|
+
backgroundColor: colors
|
|
627
|
+
}]
|
|
628
|
+
},
|
|
629
|
+
options: {
|
|
630
|
+
responsive: true,
|
|
631
|
+
maintainAspectRatio: false,
|
|
632
|
+
plugins: {
|
|
633
|
+
legend: {
|
|
634
|
+
position: 'bottom',
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Filter functionality
|
|
644
|
+
function filterTable() {
|
|
645
|
+
const searchValue = document.getElementById('searchInput').value.toLowerCase();
|
|
646
|
+
const severityValue = document.getElementById('severityFilter').value;
|
|
647
|
+
const fileValue = document.getElementById('fileFilter').value;
|
|
648
|
+
const checkValue = document.getElementById('checkFilter').value;
|
|
649
|
+
|
|
650
|
+
const rows = document.querySelectorAll('.issue-row');
|
|
651
|
+
|
|
652
|
+
rows.forEach(row => {
|
|
653
|
+
const matchesSearch = !searchValue || row.dataset.message.includes(searchValue);
|
|
654
|
+
const matchesSeverity = !severityValue || row.dataset.severity === severityValue;
|
|
655
|
+
const matchesFile = !fileValue || row.dataset.file === fileValue;
|
|
656
|
+
const matchesCheck = !checkValue || row.dataset.check === checkValue;
|
|
657
|
+
|
|
658
|
+
if (matchesSearch && matchesSeverity && matchesFile && matchesCheck) {
|
|
659
|
+
row.classList.remove('hidden');
|
|
660
|
+
} else {
|
|
661
|
+
row.classList.add('hidden');
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Attach filter event listeners
|
|
667
|
+
document.getElementById('searchInput').addEventListener('input', filterTable);
|
|
668
|
+
document.getElementById('severityFilter').addEventListener('change', filterTable);
|
|
669
|
+
document.getElementById('fileFilter').addEventListener('change', filterTable);
|
|
670
|
+
document.getElementById('checkFilter').addEventListener('change', filterTable);
|
|
671
|
+
</script>
|
|
672
|
+
"""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""JSON formatter - placeholder for existing functionality."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
6
|
+
from iam_validator.core.models import ValidationReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JSONFormatter(OutputFormatter):
|
|
10
|
+
"""JSON formatter for programmatic processing."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def format_id(self) -> str:
|
|
14
|
+
return "json"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self) -> str:
|
|
18
|
+
return "JSON format for programmatic processing"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def file_extension(self) -> str:
|
|
22
|
+
return "json"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def content_type(self) -> str:
|
|
26
|
+
return "application/json"
|
|
27
|
+
|
|
28
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
29
|
+
"""Format as JSON."""
|
|
30
|
+
# This would integrate with existing JSON output
|
|
31
|
+
# from iam_validator.core.report module
|
|
32
|
+
indent = kwargs.get("indent", 2)
|
|
33
|
+
return json.dumps(report.model_dump(), indent=indent, default=str)
|