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.
Files changed (34) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/METADATA +1 -2
  2. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +34 -33
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/action_condition_enforcement.py +20 -13
  5. iam_validator/checks/action_resource_matching.py +70 -36
  6. iam_validator/checks/condition_key_validation.py +7 -7
  7. iam_validator/checks/condition_type_mismatch.py +8 -6
  8. iam_validator/checks/full_wildcard.py +2 -8
  9. iam_validator/checks/mfa_condition_check.py +8 -8
  10. iam_validator/checks/principal_validation.py +24 -20
  11. iam_validator/checks/sensitive_action.py +3 -9
  12. iam_validator/checks/service_wildcard.py +2 -8
  13. iam_validator/checks/sid_uniqueness.py +1 -1
  14. iam_validator/checks/wildcard_action.py +2 -8
  15. iam_validator/checks/wildcard_resource.py +2 -8
  16. iam_validator/commands/validate.py +2 -2
  17. iam_validator/core/aws_fetcher.py +115 -22
  18. iam_validator/core/config/config_loader.py +1 -2
  19. iam_validator/core/config/defaults.py +16 -7
  20. iam_validator/core/constants.py +57 -0
  21. iam_validator/core/formatters/console.py +10 -1
  22. iam_validator/core/formatters/csv.py +2 -1
  23. iam_validator/core/formatters/enhanced.py +42 -8
  24. iam_validator/core/formatters/markdown.py +2 -1
  25. iam_validator/core/models.py +22 -7
  26. iam_validator/core/policy_checks.py +5 -4
  27. iam_validator/core/policy_loader.py +71 -14
  28. iam_validator/core/report.py +65 -24
  29. iam_validator/integrations/github_integration.py +4 -5
  30. iam_validator/utils/__init__.py +4 -0
  31. iam_validator/utils/terminal.py +22 -0
  32. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
  33. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
  34. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -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(self, results: list[PolicyValidationResult]) -> ValidationReport:
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 adjusted column widths for better readability
181
- table = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
182
- table.add_column("Severity", style="cyan", width=12, no_wrap=False)
183
- table.add_column("Type", style="magenta", width=25, no_wrap=False)
184
- table.add_column("Message", style="white", no_wrap=False)
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, report: ValidationReport, max_length_per_part: int = 60000
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 60000)
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
- base_overhead = 2000 # Header + footer
264
- chars_per_issue = 500
265
- return base_overhead + (report.total_issues * chars_per_issue)
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 = 200
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 ("error", "critical", "high")),
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 ("error", "critical", "high")
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 ("error", "critical", "high")]
460
- warnings = [i for i in result.issues if i.severity in ("warning", "medium")]
461
- infos = [i for i in result.issues if i.severity in ("info", "low")]
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(self, report: ValidationReport, max_length: int = 65000) -> str:
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 (GitHub limit is 65536, we use 65000 for safety)
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
- if report.invalid_policies == 0:
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
  "![Status](https://img.shields.io/badge/status-passed-success?style=flat-square)"
@@ -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 ("error", "critical", "high")
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) + 100 # 100 for safety
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 ("error", "critical", "high")),
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 ("error", "critical", "high")]
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 = "🤖 IAM Policy Validator") -> int:
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.
@@ -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