iam-policy-validator 1.7.1__py3-none-any.whl → 1.7.2__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.
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/METADATA +1 -2
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +34 -33
- iam_validator/__version__.py +4 -2
- iam_validator/checks/action_condition_enforcement.py +20 -13
- iam_validator/checks/action_resource_matching.py +70 -36
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +8 -6
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/principal_validation.py +24 -20
- iam_validator/checks/sensitive_action.py +3 -9
- iam_validator/checks/service_wildcard.py +2 -8
- iam_validator/checks/sid_uniqueness.py +1 -1
- iam_validator/checks/wildcard_action.py +2 -8
- iam_validator/checks/wildcard_resource.py +2 -8
- iam_validator/commands/validate.py +2 -2
- iam_validator/core/aws_fetcher.py +115 -22
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +16 -7
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/models.py +22 -7
- iam_validator/core/policy_checks.py +5 -4
- iam_validator/core/policy_loader.py +71 -14
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/terminal.py +22 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
iam_validator/core/report.py
CHANGED
|
@@ -12,6 +12,7 @@ from rich.table import Table
|
|
|
12
12
|
from rich.text import Text
|
|
13
13
|
|
|
14
14
|
from iam_validator.__version__ import __version__
|
|
15
|
+
from iam_validator.core import constants
|
|
15
16
|
from iam_validator.core.formatters import (
|
|
16
17
|
ConsoleFormatter,
|
|
17
18
|
CSVFormatter,
|
|
@@ -71,11 +72,16 @@ class ReportGenerator:
|
|
|
71
72
|
"""
|
|
72
73
|
return self.formatter_registry.format_report(report, format_id, **kwargs)
|
|
73
74
|
|
|
74
|
-
def generate_report(
|
|
75
|
+
def generate_report(
|
|
76
|
+
self,
|
|
77
|
+
results: list[PolicyValidationResult],
|
|
78
|
+
parsing_errors: list[tuple[str, str]] | None = None,
|
|
79
|
+
) -> ValidationReport:
|
|
75
80
|
"""Generate a validation report from results.
|
|
76
81
|
|
|
77
82
|
Args:
|
|
78
83
|
results: List of policy validation results
|
|
84
|
+
parsing_errors: Optional list of (file_path, error_message) for files that failed to parse
|
|
79
85
|
|
|
80
86
|
Returns:
|
|
81
87
|
ValidationReport
|
|
@@ -106,6 +112,7 @@ class ReportGenerator:
|
|
|
106
112
|
validity_issues=validity_issues,
|
|
107
113
|
security_issues=security_issues,
|
|
108
114
|
results=results,
|
|
115
|
+
parsing_errors=parsing_errors or [],
|
|
109
116
|
)
|
|
110
117
|
|
|
111
118
|
def print_console_report(self, report: ValidationReport) -> None:
|
|
@@ -150,6 +157,7 @@ class ReportGenerator:
|
|
|
150
157
|
summary_text,
|
|
151
158
|
title=f"Validation Summary (iam-validator v{__version__})",
|
|
152
159
|
border_style="blue",
|
|
160
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
153
161
|
)
|
|
154
162
|
)
|
|
155
163
|
|
|
@@ -177,11 +185,12 @@ class ReportGenerator:
|
|
|
177
185
|
self.console.print(" [dim]No issues found[/dim]")
|
|
178
186
|
return
|
|
179
187
|
|
|
180
|
-
# Create issues table with
|
|
181
|
-
|
|
182
|
-
table
|
|
183
|
-
table.add_column("
|
|
184
|
-
table.add_column("
|
|
188
|
+
# Create issues table with flexible column widths
|
|
189
|
+
# Use wider columns and more padding to better utilize terminal width
|
|
190
|
+
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2), expand=True)
|
|
191
|
+
table.add_column("Severity", style="cyan", no_wrap=True, min_width=12)
|
|
192
|
+
table.add_column("Type", style="magenta", no_wrap=False, min_width=32)
|
|
193
|
+
table.add_column("Message", style="white", no_wrap=False, ratio=3)
|
|
185
194
|
|
|
186
195
|
for issue in result.issues:
|
|
187
196
|
severity_style = {
|
|
@@ -207,6 +216,8 @@ class ReportGenerator:
|
|
|
207
216
|
message = f"{location}: {issue.message}"
|
|
208
217
|
if issue.suggestion:
|
|
209
218
|
message += f"\n → {issue.suggestion}"
|
|
219
|
+
if issue.example:
|
|
220
|
+
message += f"\n[dim]Example:[/dim]\n[dim]{issue.example}[/dim]"
|
|
210
221
|
|
|
211
222
|
table.add_row(severity_style, issue.issue_type, message)
|
|
212
223
|
|
|
@@ -224,13 +235,15 @@ class ReportGenerator:
|
|
|
224
235
|
return report.model_dump_json(indent=2)
|
|
225
236
|
|
|
226
237
|
def generate_github_comment_parts(
|
|
227
|
-
self,
|
|
238
|
+
self,
|
|
239
|
+
report: ValidationReport,
|
|
240
|
+
max_length_per_part: int = constants.GITHUB_COMMENT_SPLIT_LIMIT,
|
|
228
241
|
) -> list[str]:
|
|
229
242
|
"""Generate GitHub PR comment(s), splitting into multiple parts if needed.
|
|
230
243
|
|
|
231
244
|
Args:
|
|
232
245
|
report: Validation report
|
|
233
|
-
max_length_per_part: Maximum character length per comment part (default
|
|
246
|
+
max_length_per_part: Maximum character length per comment part (default from GITHUB_COMMENT_SPLIT_LIMIT)
|
|
234
247
|
|
|
235
248
|
Returns:
|
|
236
249
|
List of comment parts (each under max_length_per_part)
|
|
@@ -260,9 +273,9 @@ class ReportGenerator:
|
|
|
260
273
|
Estimated character count
|
|
261
274
|
"""
|
|
262
275
|
# Rough estimate: ~500 chars per issue + overhead
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
276
|
+
return constants.COMMENT_BASE_OVERHEAD_CHARS + (
|
|
277
|
+
report.total_issues * constants.COMMENT_CHARS_PER_ISSUE_ESTIMATE
|
|
278
|
+
)
|
|
266
279
|
|
|
267
280
|
def _generate_split_comments(self, report: ValidationReport, max_length: int) -> list[str]:
|
|
268
281
|
"""Split a large report into multiple comment parts.
|
|
@@ -289,13 +302,13 @@ class ReportGenerator:
|
|
|
289
302
|
# - Part indicator: "**(Part N/M)**\n\n" (estimated ~20 chars)
|
|
290
303
|
# - HTML comment identifier: "<!-- iam-policy-validator -->\n" (~35 chars)
|
|
291
304
|
# - Safety buffer for formatting
|
|
292
|
-
continuation_overhead =
|
|
305
|
+
continuation_overhead = constants.COMMENT_CONTINUATION_OVERHEAD_CHARS
|
|
293
306
|
|
|
294
307
|
# Sort results to prioritize errors - support both IAM validity and security severities
|
|
295
308
|
sorted_results = sorted(
|
|
296
309
|
[(idx, r) for idx, r in enumerate(report.results, 1) if r.issues],
|
|
297
310
|
key=lambda x: (
|
|
298
|
-
-sum(1 for i in x[1].issues if i.severity in
|
|
311
|
+
-sum(1 for i in x[1].issues if i.severity in constants.HIGH_SEVERITY_LEVELS),
|
|
299
312
|
-len(x[1].issues),
|
|
300
313
|
),
|
|
301
314
|
)
|
|
@@ -410,7 +423,7 @@ class ReportGenerator:
|
|
|
410
423
|
1
|
|
411
424
|
for r in report.results
|
|
412
425
|
for i in r.issues
|
|
413
|
-
if i.severity in
|
|
426
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
414
427
|
)
|
|
415
428
|
warnings = sum(
|
|
416
429
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -456,9 +469,9 @@ class ReportGenerator:
|
|
|
456
469
|
lines.append("")
|
|
457
470
|
|
|
458
471
|
# Group issues by severity - support both IAM validity and security severities
|
|
459
|
-
errors = [i for i in result.issues if i.severity in
|
|
460
|
-
warnings = [i for i in result.issues if i.severity in
|
|
461
|
-
infos = [i for i in result.issues if i.severity in
|
|
472
|
+
errors = [i for i in result.issues if i.severity in constants.HIGH_SEVERITY_LEVELS]
|
|
473
|
+
warnings = [i for i in result.issues if i.severity in constants.MEDIUM_SEVERITY_LEVELS]
|
|
474
|
+
infos = [i for i in result.issues if i.severity in constants.LOW_SEVERITY_LEVELS]
|
|
462
475
|
|
|
463
476
|
if errors:
|
|
464
477
|
lines.append("### 🔴 Errors")
|
|
@@ -510,12 +523,16 @@ class ReportGenerator:
|
|
|
510
523
|
|
|
511
524
|
return "\n".join(parts)
|
|
512
525
|
|
|
513
|
-
def generate_github_comment(
|
|
526
|
+
def generate_github_comment(
|
|
527
|
+
self,
|
|
528
|
+
report: ValidationReport,
|
|
529
|
+
max_length: int = constants.GITHUB_MAX_COMMENT_LENGTH,
|
|
530
|
+
) -> str:
|
|
514
531
|
"""Generate a GitHub-flavored markdown comment for PR reviews.
|
|
515
532
|
|
|
516
533
|
Args:
|
|
517
534
|
report: Validation report
|
|
518
|
-
max_length: Maximum character length (
|
|
535
|
+
max_length: Maximum character length (default from GITHUB_MAX_COMMENT_LENGTH constant)
|
|
519
536
|
|
|
520
537
|
Returns:
|
|
521
538
|
Markdown formatted string
|
|
@@ -523,7 +540,8 @@ class ReportGenerator:
|
|
|
523
540
|
lines = []
|
|
524
541
|
|
|
525
542
|
# Header with emoji and status badge
|
|
526
|
-
|
|
543
|
+
has_parsing_errors = len(report.parsing_errors) > 0
|
|
544
|
+
if report.invalid_policies == 0 and not has_parsing_errors:
|
|
527
545
|
lines.append("# 🎉 IAM Policy Validation Passed!")
|
|
528
546
|
status_badge = (
|
|
529
547
|
""
|
|
@@ -558,7 +576,7 @@ class ReportGenerator:
|
|
|
558
576
|
1
|
|
559
577
|
for r in report.results
|
|
560
578
|
for i in r.issues
|
|
561
|
-
if i.severity in
|
|
579
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
562
580
|
)
|
|
563
581
|
warnings = sum(
|
|
564
582
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -579,6 +597,29 @@ class ReportGenerator:
|
|
|
579
597
|
lines.append(f"| 🔵 **Info** | {infos} |")
|
|
580
598
|
lines.append("")
|
|
581
599
|
|
|
600
|
+
# Parsing errors section (if any)
|
|
601
|
+
if report.parsing_errors:
|
|
602
|
+
lines.append("### ⚠️ Parsing Errors")
|
|
603
|
+
lines.append("")
|
|
604
|
+
lines.append(
|
|
605
|
+
f"**{len(report.parsing_errors)} file(s) failed to parse** and were excluded from validation:"
|
|
606
|
+
)
|
|
607
|
+
lines.append("")
|
|
608
|
+
for file_path, error_msg in report.parsing_errors:
|
|
609
|
+
# Extract just the filename for cleaner display
|
|
610
|
+
from pathlib import Path
|
|
611
|
+
|
|
612
|
+
filename = Path(file_path).name
|
|
613
|
+
lines.append(f"- **`{filename}`**")
|
|
614
|
+
lines.append(" ```")
|
|
615
|
+
lines.append(f" {error_msg}")
|
|
616
|
+
lines.append(" ```")
|
|
617
|
+
lines.append("")
|
|
618
|
+
lines.append(
|
|
619
|
+
"> **Note:** Fix these parsing errors first before validation can proceed on these files."
|
|
620
|
+
)
|
|
621
|
+
lines.append("")
|
|
622
|
+
|
|
582
623
|
# Store header for later (we always include this)
|
|
583
624
|
header_content = "\n".join(lines)
|
|
584
625
|
|
|
@@ -594,7 +635,7 @@ class ReportGenerator:
|
|
|
594
635
|
footer_content = "\n".join(footer_lines)
|
|
595
636
|
|
|
596
637
|
# Calculate remaining space for details
|
|
597
|
-
base_length = len(header_content) + len(footer_content) +
|
|
638
|
+
base_length = len(header_content) + len(footer_content) + constants.FORMATTING_SAFETY_BUFFER
|
|
598
639
|
available_length = max_length - base_length
|
|
599
640
|
|
|
600
641
|
# Detailed findings
|
|
@@ -611,7 +652,7 @@ class ReportGenerator:
|
|
|
611
652
|
sorted_results = sorted(
|
|
612
653
|
[(idx, r) for idx, r in enumerate(report.results, 1) if r.issues],
|
|
613
654
|
key=lambda x: (
|
|
614
|
-
-sum(1 for i in x[1].issues if i.severity in
|
|
655
|
+
-sum(1 for i in x[1].issues if i.severity in constants.HIGH_SEVERITY_LEVELS),
|
|
615
656
|
-len(x[1].issues),
|
|
616
657
|
),
|
|
617
658
|
)
|
|
@@ -623,7 +664,7 @@ class ReportGenerator:
|
|
|
623
664
|
policy_lines = []
|
|
624
665
|
|
|
625
666
|
# Group issues by severity - support both IAM validity and security severities
|
|
626
|
-
errors = [i for i in result.issues if i.severity in
|
|
667
|
+
errors = [i for i in result.issues if i.severity in constants.HIGH_SEVERITY_LEVELS]
|
|
627
668
|
warnings = [i for i in result.issues if i.severity in ("warning", "medium")]
|
|
628
669
|
infos = [i for i in result.issues if i.severity in ("info", "low")]
|
|
629
670
|
|
|
@@ -6,11 +6,14 @@ including posting PR comments, line comments, labels, and retrieving PR informat
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
|
+
import re
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
14
|
|
|
15
|
+
from iam_validator.core import constants
|
|
16
|
+
|
|
14
17
|
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -134,8 +137,6 @@ class GitHubIntegration:
|
|
|
134
137
|
|
|
135
138
|
# Basic sanitization - alphanumeric, hyphens, underscores, dots
|
|
136
139
|
# GitHub allows these characters in usernames and repo names
|
|
137
|
-
import re
|
|
138
|
-
|
|
139
140
|
valid_pattern = re.compile(r"^[a-zA-Z0-9._-]+$")
|
|
140
141
|
if not valid_pattern.match(owner) or not valid_pattern.match(repo):
|
|
141
142
|
logger.warning(
|
|
@@ -199,8 +200,6 @@ class GitHubIntegration:
|
|
|
199
200
|
return "https://api.github.com"
|
|
200
201
|
|
|
201
202
|
# Basic URL validation
|
|
202
|
-
import re
|
|
203
|
-
|
|
204
203
|
# Simple URL pattern check
|
|
205
204
|
url_pattern = re.compile(r"^https://[a-zA-Z0-9.-]+(?:/.*)?$")
|
|
206
205
|
if not url_pattern.match(api_url):
|
|
@@ -506,7 +505,7 @@ class GitHubIntegration:
|
|
|
506
505
|
return True
|
|
507
506
|
return False
|
|
508
507
|
|
|
509
|
-
async def cleanup_bot_review_comments(self, identifier: str =
|
|
508
|
+
async def cleanup_bot_review_comments(self, identifier: str = constants.BOT_IDENTIFIER) -> int:
|
|
510
509
|
"""Delete all review comments from the bot (from previous runs).
|
|
511
510
|
|
|
512
511
|
This ensures old/outdated comments are removed before posting new ones.
|
iam_validator/utils/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ see iam_validator.checks.utils instead.
|
|
|
10
10
|
Organization:
|
|
11
11
|
- cache.py: Generic caching implementations (LRUCache with TTL)
|
|
12
12
|
- regex.py: Regex pattern caching and compilation utilities
|
|
13
|
+
- terminal.py: Terminal width detection utilities
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from iam_validator.utils.cache import LRUCache
|
|
@@ -19,6 +20,7 @@ from iam_validator.utils.regex import (
|
|
|
19
20
|
compile_and_cache,
|
|
20
21
|
get_cached_pattern,
|
|
21
22
|
)
|
|
23
|
+
from iam_validator.utils.terminal import get_terminal_width
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
# Cache utilities
|
|
@@ -28,4 +30,6 @@ __all__ = [
|
|
|
28
30
|
"compile_and_cache",
|
|
29
31
|
"get_cached_pattern",
|
|
30
32
|
"clear_pattern_cache",
|
|
33
|
+
# Terminal utilities
|
|
34
|
+
"get_terminal_width",
|
|
31
35
|
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Terminal utilities for console output formatting."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_terminal_width(min_width: int = 80, max_width: int = 150, fallback: int = 100) -> int:
|
|
7
|
+
"""Get the current terminal width with reasonable bounds.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
min_width: Minimum width to return (default: 80)
|
|
11
|
+
max_width: Maximum width to return (default: 150)
|
|
12
|
+
fallback: Fallback width if detection fails (default: 100)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Terminal width within the specified bounds
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
19
|
+
# Ensure width is within reasonable bounds
|
|
20
|
+
return max(min(terminal_width, max_width), min_width)
|
|
21
|
+
except Exception:
|
|
22
|
+
return fallback
|
|
File without changes
|
{iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|