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.

Files changed (83) hide show
  1. iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
  2. iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
  3. iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.7.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 +7 -0
  9. iam_validator/checks/__init__.py +43 -0
  10. iam_validator/checks/action_condition_enforcement.py +884 -0
  11. iam_validator/checks/action_resource_matching.py +441 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +92 -0
  14. iam_validator/checks/condition_type_mismatch.py +259 -0
  15. iam_validator/checks/full_wildcard.py +71 -0
  16. iam_validator/checks/mfa_condition_check.py +112 -0
  17. iam_validator/checks/policy_size.py +147 -0
  18. iam_validator/checks/policy_type_validation.py +305 -0
  19. iam_validator/checks/principal_validation.py +776 -0
  20. iam_validator/checks/resource_validation.py +138 -0
  21. iam_validator/checks/sensitive_action.py +254 -0
  22. iam_validator/checks/service_wildcard.py +107 -0
  23. iam_validator/checks/set_operator_validation.py +157 -0
  24. iam_validator/checks/sid_uniqueness.py +170 -0
  25. iam_validator/checks/utils/__init__.py +1 -0
  26. iam_validator/checks/utils/policy_level_checks.py +143 -0
  27. iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
  28. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  29. iam_validator/checks/wildcard_action.py +67 -0
  30. iam_validator/checks/wildcard_resource.py +135 -0
  31. iam_validator/commands/__init__.py +25 -0
  32. iam_validator/commands/analyze.py +531 -0
  33. iam_validator/commands/base.py +48 -0
  34. iam_validator/commands/cache.py +392 -0
  35. iam_validator/commands/download_services.py +255 -0
  36. iam_validator/commands/post_to_pr.py +86 -0
  37. iam_validator/commands/validate.py +600 -0
  38. iam_validator/core/__init__.py +14 -0
  39. iam_validator/core/access_analyzer.py +671 -0
  40. iam_validator/core/access_analyzer_report.py +640 -0
  41. iam_validator/core/aws_fetcher.py +940 -0
  42. iam_validator/core/check_registry.py +607 -0
  43. iam_validator/core/cli.py +134 -0
  44. iam_validator/core/condition_validators.py +626 -0
  45. iam_validator/core/config/__init__.py +81 -0
  46. iam_validator/core/config/aws_api.py +35 -0
  47. iam_validator/core/config/aws_global_conditions.py +160 -0
  48. iam_validator/core/config/category_suggestions.py +104 -0
  49. iam_validator/core/config/condition_requirements.py +155 -0
  50. iam_validator/core/config/config_loader.py +472 -0
  51. iam_validator/core/config/defaults.py +523 -0
  52. iam_validator/core/config/principal_requirements.py +421 -0
  53. iam_validator/core/config/sensitive_actions.py +672 -0
  54. iam_validator/core/config/service_principals.py +95 -0
  55. iam_validator/core/config/wildcards.py +124 -0
  56. iam_validator/core/constants.py +74 -0
  57. iam_validator/core/formatters/__init__.py +27 -0
  58. iam_validator/core/formatters/base.py +147 -0
  59. iam_validator/core/formatters/console.py +59 -0
  60. iam_validator/core/formatters/csv.py +170 -0
  61. iam_validator/core/formatters/enhanced.py +440 -0
  62. iam_validator/core/formatters/html.py +672 -0
  63. iam_validator/core/formatters/json.py +33 -0
  64. iam_validator/core/formatters/markdown.py +63 -0
  65. iam_validator/core/formatters/sarif.py +251 -0
  66. iam_validator/core/models.py +327 -0
  67. iam_validator/core/policy_checks.py +656 -0
  68. iam_validator/core/policy_loader.py +396 -0
  69. iam_validator/core/pr_commenter.py +424 -0
  70. iam_validator/core/report.py +872 -0
  71. iam_validator/integrations/__init__.py +28 -0
  72. iam_validator/integrations/github_integration.py +815 -0
  73. iam_validator/integrations/ms_teams.py +442 -0
  74. iam_validator/sdk/__init__.py +187 -0
  75. iam_validator/sdk/arn_matching.py +382 -0
  76. iam_validator/sdk/context.py +222 -0
  77. iam_validator/sdk/exceptions.py +48 -0
  78. iam_validator/sdk/helpers.py +177 -0
  79. iam_validator/sdk/policy_utils.py +425 -0
  80. iam_validator/sdk/shortcuts.py +283 -0
  81. iam_validator/utils/__init__.py +31 -0
  82. iam_validator/utils/cache.py +105 -0
  83. 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)