iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.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.
Files changed (51) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
  2. iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +81 -36
  6. iam_validator/checks/action_resource_matching.py +75 -37
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +7 -7
  9. iam_validator/checks/condition_type_mismatch.py +10 -8
  10. iam_validator/checks/full_wildcard.py +2 -8
  11. iam_validator/checks/mfa_condition_check.py +8 -8
  12. iam_validator/checks/policy_structure.py +577 -0
  13. iam_validator/checks/policy_type_validation.py +48 -32
  14. iam_validator/checks/principal_validation.py +86 -150
  15. iam_validator/checks/resource_validation.py +8 -8
  16. iam_validator/checks/sensitive_action.py +9 -11
  17. iam_validator/checks/service_wildcard.py +4 -10
  18. iam_validator/checks/set_operator_validation.py +11 -11
  19. iam_validator/checks/sid_uniqueness.py +8 -4
  20. iam_validator/checks/trust_policy_validation.py +512 -0
  21. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +5 -9
  24. iam_validator/checks/wildcard_resource.py +5 -9
  25. iam_validator/commands/validate.py +8 -14
  26. iam_validator/core/__init__.py +1 -2
  27. iam_validator/core/access_analyzer.py +1 -1
  28. iam_validator/core/access_analyzer_report.py +2 -2
  29. iam_validator/core/aws_fetcher.py +159 -64
  30. iam_validator/core/check_registry.py +83 -79
  31. iam_validator/core/config/condition_requirements.py +69 -17
  32. iam_validator/core/config/config_loader.py +1 -2
  33. iam_validator/core/config/defaults.py +74 -59
  34. iam_validator/core/config/service_principals.py +40 -3
  35. iam_validator/core/constants.py +57 -0
  36. iam_validator/core/formatters/console.py +10 -1
  37. iam_validator/core/formatters/csv.py +2 -1
  38. iam_validator/core/formatters/enhanced.py +42 -8
  39. iam_validator/core/formatters/markdown.py +2 -1
  40. iam_validator/core/ignore_patterns.py +297 -0
  41. iam_validator/core/models.py +35 -10
  42. iam_validator/core/policy_checks.py +34 -474
  43. iam_validator/core/policy_loader.py +98 -18
  44. iam_validator/core/report.py +65 -24
  45. iam_validator/integrations/github_integration.py +4 -5
  46. iam_validator/utils/__init__.py +4 -0
  47. iam_validator/utils/terminal.py +22 -0
  48. iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
  49. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  50. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  51. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -29,8 +29,10 @@ import json
29
29
  import logging
30
30
  from collections.abc import Generator
31
31
  from pathlib import Path
32
+ from typing import overload
32
33
 
33
34
  import yaml
35
+ from pydantic import ValidationError
34
36
 
35
37
  from iam_validator.core.models import IAMPolicy
36
38
 
@@ -44,6 +46,8 @@ class PolicyLoader:
44
46
  """
45
47
 
46
48
  SUPPORTED_EXTENSIONS = {".json", ".yaml", ".yml"}
49
+ # Directories to skip when scanning recursively (cache, build artifacts, etc.)
50
+ SKIP_DIRECTORIES = {".cache", ".git", "node_modules", "__pycache__", ".venv", "venv"}
47
51
 
48
52
  def __init__(self, max_file_size_mb: int = 100) -> None:
49
53
  """Initialize the policy loader.
@@ -53,6 +57,8 @@ class PolicyLoader:
53
57
  """
54
58
  self.loaded_policies: list[tuple[str, IAMPolicy]] = []
55
59
  self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
60
+ # Track parsing/validation errors for reporting
61
+ self.parsing_errors: list[tuple[str, str]] = [] # (file_path, error_message)
56
62
 
57
63
  @staticmethod
58
64
  def _find_statement_line_numbers(file_content: str) -> list[int]:
@@ -142,26 +148,38 @@ class PolicyLoader:
142
148
  return False
143
149
  return True
144
150
  except OSError as e:
145
- logger.error(f"Failed to check file size for {path}: {e}")
151
+ logger.error("Failed to check file size for %s: %s", path, e)
146
152
  return False
147
153
 
148
- def load_from_file(self, file_path: str) -> IAMPolicy | None:
154
+ @overload
155
+ def load_from_file(self, file_path: str, return_raw_dict: bool = False) -> IAMPolicy | None: ...
156
+
157
+ @overload
158
+ def load_from_file(
159
+ self, file_path: str, return_raw_dict: bool = True
160
+ ) -> tuple[IAMPolicy, dict] | None: ...
161
+
162
+ def load_from_file(
163
+ self, file_path: str, return_raw_dict: bool = False
164
+ ) -> IAMPolicy | tuple[IAMPolicy, dict] | None:
149
165
  """Load a single IAM policy from a file.
150
166
 
151
167
  Args:
152
168
  file_path: Path to the policy file
169
+ return_raw_dict: If True, return tuple of (policy, raw_dict) for validation
153
170
 
154
171
  Returns:
155
- Parsed IAMPolicy or None if loading fails
172
+ Parsed IAMPolicy, or tuple of (IAMPolicy, raw_dict) if return_raw_dict=True,
173
+ or None if loading fails
156
174
  """
157
175
  path = Path(file_path)
158
176
 
159
177
  if not path.exists():
160
- logger.error(f"File not found: {file_path}")
178
+ logger.error("File not found: %s", file_path)
161
179
  return None
162
180
 
163
181
  if not path.is_file():
164
- logger.error(f"Not a file: {file_path}")
182
+ logger.error("Not a file: %s", file_path)
165
183
  return None
166
184
 
167
185
  if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
@@ -193,21 +211,52 @@ class PolicyLoader:
193
211
 
194
212
  # Attach line numbers to statements
195
213
  if statement_line_numbers:
196
- for idx, statement in enumerate(policy.statement):
214
+ for idx, statement in enumerate(policy.statement or []):
197
215
  if idx < len(statement_line_numbers):
198
216
  statement.line_number = statement_line_numbers[idx]
199
217
 
200
- logger.info(f"Successfully loaded policy from {file_path}")
201
- return policy
218
+ logger.info("Successfully loaded policy from %s", file_path)
219
+ return (policy, data) if return_raw_dict else policy
202
220
 
203
221
  except json.JSONDecodeError as e:
204
- logger.error(f"Invalid JSON in {file_path}: {e}")
222
+ error_msg = f"Invalid JSON: {e}"
223
+ logger.error("Invalid JSON in %s: %s", file_path, e)
224
+ self.parsing_errors.append((file_path, error_msg))
205
225
  return None
206
226
  except yaml.YAMLError as e:
207
- logger.error(f"Invalid YAML in {file_path}: {e}")
227
+ error_msg = f"Invalid YAML: {e}"
228
+ logger.error("Invalid YAML in %s: %s", file_path, e)
229
+ self.parsing_errors.append((file_path, error_msg))
230
+ return None
231
+ except ValidationError as e:
232
+ # Handle Pydantic validation errors with helpful messages
233
+ error_messages = []
234
+ for error in e.errors():
235
+ loc = ".".join(str(x) for x in error["loc"])
236
+ error_type = error["type"]
237
+
238
+ # Provide user-friendly messages for common errors
239
+ if error_type == "extra_forbidden":
240
+ # Extract the field name that has a typo
241
+ field_name = error["loc"][-1] if error["loc"] else "unknown"
242
+ error_messages.append(
243
+ f"Unknown field '{field_name}' at {loc}. "
244
+ f"This might be a typo. Did you mean 'Condition', 'Action', or 'Resource'?"
245
+ )
246
+ else:
247
+ error_messages.append(f"{loc}: {error['msg']}")
248
+
249
+ error_summary = "\n ".join(error_messages)
250
+ logger.error(
251
+ "Policy validation failed for %s:\n %s",
252
+ file_path,
253
+ error_summary,
254
+ )
255
+ # Track parsing error for GitHub reporting
256
+ self.parsing_errors.append((file_path, error_summary))
208
257
  return None
209
258
  except Exception as e:
210
- logger.error(f"Failed to load policy from {file_path}: {e}")
259
+ logger.error("Failed to load policy from %s: %s", file_path, e)
211
260
  return None
212
261
 
213
262
  def load_from_directory(
@@ -225,23 +274,27 @@ class PolicyLoader:
225
274
  path = Path(directory_path)
226
275
 
227
276
  if not path.exists():
228
- logger.error(f"Directory not found: {directory_path}")
277
+ logger.error("Directory not found: %s", directory_path)
229
278
  return []
230
279
 
231
280
  if not path.is_dir():
232
- logger.error(f"Not a directory: {directory_path}")
281
+ logger.error("Not a directory: %s", directory_path)
233
282
  return []
234
283
 
235
284
  policies: list[tuple[str, IAMPolicy]] = []
236
285
  pattern = "**/*" if recursive else "*"
237
286
 
238
287
  for file_path in path.glob(pattern):
288
+ # Skip directories that shouldn't be scanned
289
+ if any(skip_dir in file_path.parts for skip_dir in self.SKIP_DIRECTORIES):
290
+ continue
291
+
239
292
  if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
240
293
  policy = self.load_from_file(str(file_path))
241
294
  if policy:
242
295
  policies.append((str(file_path), policy))
243
296
 
244
- logger.info(f"Loaded {len(policies)} policies from {directory_path}")
297
+ logger.info("Loaded %d policies from %s", len(policies), directory_path)
245
298
  return policies
246
299
 
247
300
  def load_from_path(self, path: str, recursive: bool = True) -> list[tuple[str, IAMPolicy]]:
@@ -262,7 +315,7 @@ class PolicyLoader:
262
315
  elif path_obj.is_dir():
263
316
  return self.load_from_directory(path, recursive)
264
317
  else:
265
- logger.error(f"Path not found: {path}")
318
+ logger.error("Path not found: %s", path)
266
319
  return []
267
320
 
268
321
  def load_from_paths(
@@ -283,7 +336,7 @@ class PolicyLoader:
283
336
  policies = self.load_from_path(path.strip(), recursive)
284
337
  all_policies.extend(policies)
285
338
 
286
- logger.info(f"Loaded {len(all_policies)} total policies from {len(paths)} path(s)")
339
+ logger.info("Loaded %d total policies from %d path(s)", len(all_policies), len(paths))
287
340
  return all_policies
288
341
 
289
342
  def _get_policy_files(self, path: str, recursive: bool = True) -> Generator[Path, None, None]:
@@ -307,10 +360,14 @@ class PolicyLoader:
307
360
  elif path_obj.is_dir():
308
361
  pattern = "**/*" if recursive else "*"
309
362
  for file_path in path_obj.glob(pattern):
363
+ # Skip directories that shouldn't be scanned
364
+ if any(skip_dir in file_path.parts for skip_dir in self.SKIP_DIRECTORIES):
365
+ continue
366
+
310
367
  if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
311
368
  yield file_path
312
369
  else:
313
- logger.error(f"Path not found: {path}")
370
+ logger.error("Path not found: %s", path)
314
371
 
315
372
  def stream_from_path(
316
373
  self, path: str, recursive: bool = True
@@ -391,6 +448,29 @@ class PolicyLoader:
391
448
  policy = IAMPolicy.model_validate(data)
392
449
  logger.info("Successfully parsed policy from string")
393
450
  return policy
451
+ except json.JSONDecodeError as e:
452
+ logger.error("Invalid JSON: %s", e)
453
+ return None
454
+ except ValidationError as e:
455
+ # Handle Pydantic validation errors with helpful messages
456
+ error_messages = []
457
+ for error in e.errors():
458
+ loc = ".".join(str(x) for x in error["loc"])
459
+ error_type = error["type"]
460
+
461
+ # Provide user-friendly messages for common errors
462
+ if error_type == "extra_forbidden":
463
+ # Extract the field name that has a typo
464
+ field_name = error["loc"][-1] if error["loc"] else "unknown"
465
+ error_messages.append(
466
+ f"Unknown field '{field_name}' at {loc}. "
467
+ f"This might be a typo. Did you mean 'Condition', 'Action', or 'Resource'?"
468
+ )
469
+ else:
470
+ error_messages.append(f"{loc}: {error['msg']}")
471
+
472
+ logger.error("Policy validation failed:\n %s", "\n ".join(error_messages))
473
+ return None
394
474
  except Exception as e:
395
- logger.error(f"Failed to parse policy string: {e}")
475
+ logger.error("Failed to parse policy string: %s", e)
396
476
  return None
@@ -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
@@ -1,83 +0,0 @@
1
- iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
- iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=hhtguX-fvQWNuUzOdKMFXPyDTbhXhMm7VQgXxQD2xCQ,206
4
- iam_validator/checks/__init__.py,sha256=eDiDlVon0CwWGSBnZgM-arn1i5R5ZSG89pgR-ifETxE,1782
5
- iam_validator/checks/action_condition_enforcement.py,sha256=n-F7NEmQm76Hs-Aj5qxgXney3MpkzbWElZUu1Ig73pw,36723
6
- iam_validator/checks/action_resource_matching.py,sha256=X9dqWy1s_-h1rA81wZRLOxAVLmUHlGVPjxMo0WKIlwM,17433
7
- iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
8
- iam_validator/checks/condition_key_validation.py,sha256=E-doe2QjvKSkyjXZO9TBp0QS7M0Fv2oYYQQ9738QNxg,3918
9
- iam_validator/checks/condition_type_mismatch.py,sha256=qAbP6pP_vM1aBvIBRHji56XLH_5cQI4cDhpMQe19CHM,10588
10
- iam_validator/checks/full_wildcard.py,sha256=0_F6h4goWlc3DuZwo1F9YGw5hvpnkfZxYSDxhXXK50I,2449
11
- iam_validator/checks/mfa_condition_check.py,sha256=s7K2r9hxlJI1KWk8qXl-JOWE6jLIhpxooK26Pr7acKs,4915
12
- iam_validator/checks/policy_size.py,sha256=ibgmrErpkz6OfUAN6bFuHe1KHzpzzra9gHwNtVAkPWc,5729
13
- iam_validator/checks/policy_type_validation.py,sha256=9qmrA8CXwsVpCU4rT0RrqDXgVOzNamMEpdg3cXWAtBI,15213
14
- iam_validator/checks/principal_validation.py,sha256=Bm4pH6eiJLDa9ID7UyM63phgffh-P5DpPpSBUbYyVn8,29851
15
- iam_validator/checks/resource_validation.py,sha256=fGi9QuX-lIHDtLm8xB3VReFFhbZpQ2Yub-FKRafQCkw,5984
16
- iam_validator/checks/sensitive_action.py,sha256=mdl4g67HBioYTvAvar9CaTjxfaPvpYkNo9phL4E1c1w,9794
17
- iam_validator/checks/service_wildcard.py,sha256=CiQQoti06nqVgvH-HpBIjoW23tnTJqDU4S-ZnM1DwsA,4218
18
- iam_validator/checks/set_operator_validation.py,sha256=1XjOdf-xk-m6m1bODuHsELZccriGqOJTDI-HCcuId80,7464
19
- iam_validator/checks/sid_uniqueness.py,sha256=1Ux9W1hPPhzgdCzfxwxvD-nSBRo1SyrxFWlnTXDcOys,6887
20
- iam_validator/checks/wildcard_action.py,sha256=XAVuk5L9dQqiWPgd3HJXGNmYr2bh2szJMVVcHSBXb_8,2140
21
- iam_validator/checks/wildcard_resource.py,sha256=IEpyoU4mA3t2kRxSwVavtROYIyF0Bq1xZJAL9P7XbVQ,5582
22
- iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
23
- iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
24
- iam_validator/checks/utils/sensitive_action_matcher.py,sha256=tcWK4nImpSVNia0FUsN2uLK9LM5EnzjRFtaPQLHZaLw,10667
25
- iam_validator/checks/utils/wildcard_expansion.py,sha256=fSSoquVdVZaVWS_qBxAx7LMOzxgHed4ffQ6OAZnuqos,3132
26
- iam_validator/commands/__init__.py,sha256=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
27
- iam_validator/commands/analyze.py,sha256=rvLBJ5_A3HB530xtixhaIsC19QON68olEQnn8TievgI,20784
28
- iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
29
- iam_validator/commands/cache.py,sha256=p4ucRVuh42sbK3Lk0b610L3ofAR5TnUreF00fpO6VFg,14219
30
- iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
31
- iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
32
- iam_validator/commands/validate.py,sha256=Eik-w613zCnX7hUHziBq4k5la3e3qJ0CO1__7aw-gBk,23554
33
- iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
34
- iam_validator/core/access_analyzer.py,sha256=8GgkR-vCkCtSxtXGywvQNBPYq-rvDLexUuLSyflq0V4,24520
35
- iam_validator/core/access_analyzer_report.py,sha256=O17gagknvkNMTTlq7BrLM68FjlCEm4LjIKD9oqxEbPg,24860
36
- iam_validator/core/aws_fetcher.py,sha256=cZFo5JMSoNLx1tpM6NzYr2cnq8Bvc2KQx2nJDmo69lc,36504
37
- iam_validator/core/check_registry.py,sha256=cMjtJROkZOLzXxl-mTdLYHdxyajNnOsaHGs-EeaSZ7k,21741
38
- iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
39
- iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
40
- iam_validator/core/constants.py,sha256=oblMWsjoroIhwjYgZdcyLxaATsGeR99zQwRg6h59Nlo,3145
41
- iam_validator/core/models.py,sha256=59yqvHoX3nCSJyQDmWCuEsQzNz9PNiF7um7A1wti-2w,12176
42
- iam_validator/core/policy_checks.py,sha256=Uz2yCsqRaoIja31F4ZM-39a1pHv51yZqKyWWkGUZKNY,26489
43
- iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
44
- iam_validator/core/pr_commenter.py,sha256=MU-t7SfdHUpSc6BDbh8_dNAbxDiG-bZBCry-jUXivAc,15066
45
- iam_validator/core/report.py,sha256=j6uWlFL6Xavl4BnpaQtQoxFOEgKEiuY0IYBq8I9DH5Q,34134
46
- iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
47
- iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
48
- iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
49
- iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
50
- iam_validator/core/config/condition_requirements.py,sha256=1PuADTB9pLqh-kNUGC7kSU6LMLtXMSc003tvI7qKeAY,5170
51
- iam_validator/core/config/config_loader.py,sha256=7YkuPnroR-Up5CUTQOXIyS_b732WrzNn8o1EH9O6lyI,17730
52
- iam_validator/core/config/defaults.py,sha256=w5ievxkqki3zYr7NaREoWtVx5rTfxBpZlgoNdovcILs,27112
53
- iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
54
- iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
55
- iam_validator/core/config/service_principals.py,sha256=gQSROsxUWBD6P2F9qP320UZV4lHGlsyvHSkMyy0njrU,2685
56
- iam_validator/core/config/wildcards.py,sha256=H_v6hb-rZ0UUz4cul9lxkVI39e6knaK4Y-MbWz2Ebpw,3228
57
- iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
58
- iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
59
- iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
60
- iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
61
- iam_validator/core/formatters/enhanced.py,sha256=S0UgYKFOgILfOqwnBC8-WFab3F1CiEko33g0nbaswtk,17085
62
- iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
63
- iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
64
- iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
65
- iam_validator/core/formatters/sarif.py,sha256=O3pn7whqFq5xxk-tuoqSb2k4Fk5ai_A2SKX_ph8GLV4,10469
66
- iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
67
- iam_validator/integrations/github_integration.py,sha256=QoPkaxdRDQTzmHN4cKEXoGcn8BRv37JW4IvD2W5jEtc,26474
68
- iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
69
- iam_validator/sdk/__init__.py,sha256=fRDSXAclGmCU3KDft4StL8JUcpAsdzwIRf8mVj461q0,5306
70
- iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urchEFDA,12778
71
- iam_validator/sdk/context.py,sha256=SBFeedu8rhCzFA-zC2cH4wLZxEJT6XOW30hIZAyXPVU,6826
72
- iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
73
- iam_validator/sdk/helpers.py,sha256=OVBg4xrW95LT74wXCg1LQkba9kw5RfFqeCLuTqhgL-A,5697
74
- iam_validator/sdk/policy_utils.py,sha256=CZS1OGSdiWsd2lsCwg0BDcUNWa61tUwgvn-P5rKqeN8,12987
75
- iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
76
- iam_validator/utils/__init__.py,sha256=V8u-SSdnL4a7NwF-yg9x0JRl5epKAXEs2f5RiwK2qPo,856
77
- iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
78
- iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
79
- iam_policy_validator-1.7.1.dist-info/METADATA,sha256=WCjnDcJ38j-LRUz2EwdT2b2lX2IOTkW3xT1SLtCxiWY,15343
80
- iam_policy_validator-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- iam_policy_validator-1.7.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
82
- iam_policy_validator-1.7.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
83
- iam_policy_validator-1.7.1.dist-info/RECORD,,