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
|
@@ -39,8 +39,17 @@ class ConsoleFormatter(OutputFormatter):
|
|
|
39
39
|
color = kwargs.get("color", True)
|
|
40
40
|
|
|
41
41
|
# Capture the output from print_console_report
|
|
42
|
+
from iam_validator.utils import get_terminal_width
|
|
43
|
+
|
|
42
44
|
string_buffer = StringIO()
|
|
43
|
-
|
|
45
|
+
# Get terminal width for proper table column spacing
|
|
46
|
+
terminal_width = get_terminal_width()
|
|
47
|
+
console = Console(
|
|
48
|
+
file=string_buffer,
|
|
49
|
+
force_terminal=color,
|
|
50
|
+
width=terminal_width,
|
|
51
|
+
legacy_windows=False,
|
|
52
|
+
)
|
|
44
53
|
|
|
45
54
|
# Create a generator instance with our custom console
|
|
46
55
|
generator = ReportGenerator()
|
|
@@ -4,6 +4,7 @@ import csv
|
|
|
4
4
|
import io
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from iam_validator.core import constants
|
|
7
8
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
8
9
|
from iam_validator.core.models import ValidationReport
|
|
9
10
|
|
|
@@ -58,7 +59,7 @@ class CSVFormatter(OutputFormatter):
|
|
|
58
59
|
1
|
|
59
60
|
for r in report.results
|
|
60
61
|
for i in r.issues
|
|
61
|
-
if i.severity in
|
|
62
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
62
63
|
)
|
|
63
64
|
warnings = sum(
|
|
64
65
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -10,6 +10,7 @@ from rich.text import Text
|
|
|
10
10
|
from rich.tree import Tree
|
|
11
11
|
|
|
12
12
|
from iam_validator.__version__ import __version__
|
|
13
|
+
from iam_validator.core import constants
|
|
13
14
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
14
15
|
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
15
16
|
|
|
@@ -50,8 +51,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
50
51
|
show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
|
|
51
52
|
|
|
52
53
|
# Use StringIO to capture Rich console output
|
|
54
|
+
from iam_validator.utils import get_terminal_width
|
|
55
|
+
|
|
53
56
|
string_buffer = StringIO()
|
|
54
|
-
|
|
57
|
+
# Get terminal width for proper text wrapping
|
|
58
|
+
terminal_width = get_terminal_width()
|
|
59
|
+
console = Console(
|
|
60
|
+
file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
|
|
61
|
+
)
|
|
55
62
|
|
|
56
63
|
# Header with title
|
|
57
64
|
console.print()
|
|
@@ -60,7 +67,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
60
67
|
style="bold cyan",
|
|
61
68
|
justify="center",
|
|
62
69
|
)
|
|
63
|
-
console.print(
|
|
70
|
+
console.print(
|
|
71
|
+
Panel(
|
|
72
|
+
title,
|
|
73
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
74
|
+
padding=(1, 0),
|
|
75
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
64
78
|
console.print()
|
|
65
79
|
|
|
66
80
|
# Executive Summary with progress bars (optional)
|
|
@@ -73,7 +87,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
73
87
|
self._print_severity_breakdown(console, report)
|
|
74
88
|
|
|
75
89
|
console.print()
|
|
76
|
-
console.print(Rule(title="[bold]Detailed Results", style=
|
|
90
|
+
console.print(Rule(title="[bold]Detailed Results", style=constants.CONSOLE_HEADER_COLOR))
|
|
77
91
|
console.print()
|
|
78
92
|
|
|
79
93
|
# Detailed results using tree structure
|
|
@@ -140,8 +154,9 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
140
154
|
Panel(
|
|
141
155
|
metrics_table,
|
|
142
156
|
title="📊 Executive Summary",
|
|
143
|
-
border_style=
|
|
157
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
144
158
|
padding=(1, 2),
|
|
159
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
145
160
|
)
|
|
146
161
|
)
|
|
147
162
|
|
|
@@ -225,7 +240,12 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
225
240
|
)
|
|
226
241
|
|
|
227
242
|
console.print(
|
|
228
|
-
Panel(
|
|
243
|
+
Panel(
|
|
244
|
+
severity_table,
|
|
245
|
+
title="🎯 Issue Severity Breakdown",
|
|
246
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
247
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
248
|
+
)
|
|
229
249
|
)
|
|
230
250
|
|
|
231
251
|
def _format_policy_result_modern(
|
|
@@ -247,7 +267,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
247
267
|
elif result.is_valid and result.issues:
|
|
248
268
|
# Valid IAM policy but has security findings
|
|
249
269
|
# Check severity to determine the appropriate status
|
|
250
|
-
has_critical = any(i.severity in
|
|
270
|
+
has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
|
|
251
271
|
if has_critical:
|
|
252
272
|
icon = "⚠️"
|
|
253
273
|
color = "red"
|
|
@@ -386,6 +406,13 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
386
406
|
suggestion_text.append(issue.suggestion, style="italic yellow")
|
|
387
407
|
msg_node.add(suggestion_text)
|
|
388
408
|
|
|
409
|
+
# Example (if present, show with indentation)
|
|
410
|
+
if issue.example:
|
|
411
|
+
msg_node.add(Text("Example:", style="bold cyan"))
|
|
412
|
+
# Show example code with syntax highlighting
|
|
413
|
+
example_text = Text(issue.example, style="dim")
|
|
414
|
+
msg_node.add(example_text)
|
|
415
|
+
|
|
389
416
|
def _print_final_status(self, console: Console, report: ValidationReport) -> None:
|
|
390
417
|
"""Print final status panel."""
|
|
391
418
|
if report.invalid_policies == 0 and report.total_issues == 0:
|
|
@@ -400,7 +427,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
400
427
|
# Valid IAM policies but may have security findings
|
|
401
428
|
# Check if there are critical/high security issues
|
|
402
429
|
has_critical = any(
|
|
403
|
-
i.severity in
|
|
430
|
+
i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
404
431
|
for r in report.results
|
|
405
432
|
for i in r.issues
|
|
406
433
|
)
|
|
@@ -437,4 +464,11 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
437
464
|
final_text.append("\n\n")
|
|
438
465
|
final_text.append(message)
|
|
439
466
|
|
|
440
|
-
console.print(
|
|
467
|
+
console.print(
|
|
468
|
+
Panel(
|
|
469
|
+
final_text,
|
|
470
|
+
border_style=border_color,
|
|
471
|
+
padding=(1, 2),
|
|
472
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
473
|
+
)
|
|
474
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Markdown formatter - placeholder for existing functionality."""
|
|
2
2
|
|
|
3
|
+
from iam_validator.core import constants
|
|
3
4
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
4
5
|
from iam_validator.core.models import ValidationReport
|
|
5
6
|
|
|
@@ -34,7 +35,7 @@ class MarkdownFormatter(OutputFormatter):
|
|
|
34
35
|
1
|
|
35
36
|
for r in report.results
|
|
36
37
|
for i in r.issues
|
|
37
|
-
if i.severity in
|
|
38
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
38
39
|
)
|
|
39
40
|
warnings = sum(
|
|
40
41
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
iam_validator/core/models.py
CHANGED
|
@@ -8,6 +8,8 @@ from typing import Any, ClassVar, Literal
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
11
|
+
from iam_validator.core import constants
|
|
12
|
+
|
|
11
13
|
# Policy Type Constants
|
|
12
14
|
PolicyType = Literal[
|
|
13
15
|
"IDENTITY_POLICY",
|
|
@@ -89,9 +91,8 @@ class ServiceDetail(BaseModel):
|
|
|
89
91
|
resources_list: list[ResourceType] = Field(default_factory=list, alias="Resources")
|
|
90
92
|
condition_keys_list: list[ConditionKey] = Field(default_factory=list, alias="ConditionKeys")
|
|
91
93
|
|
|
92
|
-
def model_post_init(self, __context: Any) -> None:
|
|
94
|
+
def model_post_init(self, __context: Any, /) -> None:
|
|
93
95
|
"""Convert lists to dictionaries for easier lookup."""
|
|
94
|
-
del __context # Unused
|
|
95
96
|
# Convert actions list to dict
|
|
96
97
|
self.actions = {action.name: action for action in self.actions_list}
|
|
97
98
|
# Convert resources list to dict
|
|
@@ -104,7 +105,7 @@ class ServiceDetail(BaseModel):
|
|
|
104
105
|
class Statement(BaseModel):
|
|
105
106
|
"""IAM policy statement."""
|
|
106
107
|
|
|
107
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
108
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
108
109
|
|
|
109
110
|
sid: str | None = Field(default=None, alias="Sid")
|
|
110
111
|
effect: str = Field(alias="Effect")
|
|
@@ -134,7 +135,7 @@ class Statement(BaseModel):
|
|
|
134
135
|
class IAMPolicy(BaseModel):
|
|
135
136
|
"""IAM policy document."""
|
|
136
137
|
|
|
137
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
138
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
138
139
|
|
|
139
140
|
version: str = Field(alias="Version")
|
|
140
141
|
statement: list[Statement] = Field(alias="Statement")
|
|
@@ -161,6 +162,7 @@ class ValidationIssue(BaseModel):
|
|
|
161
162
|
resource: str | None = None
|
|
162
163
|
condition_key: str | None = None
|
|
163
164
|
suggestion: str | None = None
|
|
165
|
+
example: str | None = None # Code example (JSON/YAML) - formatted separately for GitHub
|
|
164
166
|
line_number: int | None = None # Line number in the policy file (if available)
|
|
165
167
|
|
|
166
168
|
# Severity level constants (ClassVar to avoid Pydantic treating them as fields)
|
|
@@ -225,8 +227,8 @@ class ValidationIssue(BaseModel):
|
|
|
225
227
|
|
|
226
228
|
# Add identifier for bot comment cleanup (HTML comment - not visible to users)
|
|
227
229
|
if include_identifier:
|
|
228
|
-
parts.append("
|
|
229
|
-
parts.append("
|
|
230
|
+
parts.append(f"{constants.REVIEW_IDENTIFIER}\n")
|
|
231
|
+
parts.append(f"{constants.BOT_IDENTIFIER}\n")
|
|
230
232
|
|
|
231
233
|
# Build statement context for better navigation
|
|
232
234
|
statement_context = f"Statement[{self.statement_index}]"
|
|
@@ -241,7 +243,9 @@ class ValidationIssue(BaseModel):
|
|
|
241
243
|
parts.append(self.message)
|
|
242
244
|
|
|
243
245
|
# Put additional details in collapsible section if there are any
|
|
244
|
-
has_details = bool(
|
|
246
|
+
has_details = bool(
|
|
247
|
+
self.action or self.resource or self.condition_key or self.suggestion or self.example
|
|
248
|
+
)
|
|
245
249
|
|
|
246
250
|
if has_details:
|
|
247
251
|
parts.append("")
|
|
@@ -266,6 +270,14 @@ class ValidationIssue(BaseModel):
|
|
|
266
270
|
parts.append("**💡 Suggested Fix:**")
|
|
267
271
|
parts.append("")
|
|
268
272
|
parts.append(self.suggestion)
|
|
273
|
+
parts.append("")
|
|
274
|
+
|
|
275
|
+
# Add example if present (formatted as JSON code block for GitHub)
|
|
276
|
+
if self.example:
|
|
277
|
+
parts.append("**Example:**")
|
|
278
|
+
parts.append("```json")
|
|
279
|
+
parts.append(self.example)
|
|
280
|
+
parts.append("```")
|
|
269
281
|
|
|
270
282
|
parts.append("")
|
|
271
283
|
parts.append("</details>")
|
|
@@ -298,6 +310,9 @@ class ValidationReport(BaseModel):
|
|
|
298
310
|
validity_issues: int = 0 # Count of IAM validity issues (error/warning/info)
|
|
299
311
|
security_issues: int = 0 # Count of security issues (critical/high/medium/low)
|
|
300
312
|
results: list[PolicyValidationResult] = Field(default_factory=list)
|
|
313
|
+
parsing_errors: list[tuple[str, str]] = Field(
|
|
314
|
+
default_factory=list
|
|
315
|
+
) # (file_path, error_message)
|
|
301
316
|
|
|
302
317
|
def get_summary(self) -> str:
|
|
303
318
|
"""Generate a human-readable summary."""
|
|
@@ -12,6 +12,7 @@ import logging
|
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
+
from iam_validator.core import constants
|
|
15
16
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
16
17
|
from iam_validator.core.check_registry import CheckRegistry
|
|
17
18
|
from iam_validator.core.models import (
|
|
@@ -495,10 +496,10 @@ async def validate_policies(
|
|
|
495
496
|
|
|
496
497
|
config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
|
|
497
498
|
cache_enabled = config.get_setting("cache_enabled", True)
|
|
498
|
-
cache_ttl_hours = config.get_setting("cache_ttl_hours",
|
|
499
|
+
cache_ttl_hours = config.get_setting("cache_ttl_hours", constants.DEFAULT_CACHE_TTL_HOURS)
|
|
499
500
|
cache_directory = config.get_setting("cache_directory", None)
|
|
500
501
|
aws_services_dir = config.get_setting("aws_services_dir", None)
|
|
501
|
-
cache_ttl_seconds = cache_ttl_hours *
|
|
502
|
+
cache_ttl_seconds = cache_ttl_hours * constants.SECONDS_PER_HOUR
|
|
502
503
|
|
|
503
504
|
async with AWSServiceFetcher(
|
|
504
505
|
enable_cache=cache_enabled,
|
|
@@ -566,10 +567,10 @@ async def validate_policies(
|
|
|
566
567
|
|
|
567
568
|
# Get cache settings from config
|
|
568
569
|
cache_enabled = config.get_setting("cache_enabled", True)
|
|
569
|
-
cache_ttl_hours = config.get_setting("cache_ttl_hours",
|
|
570
|
+
cache_ttl_hours = config.get_setting("cache_ttl_hours", constants.DEFAULT_CACHE_TTL_HOURS)
|
|
570
571
|
cache_directory = config.get_setting("cache_directory", None)
|
|
571
572
|
aws_services_dir = config.get_setting("aws_services_dir", None)
|
|
572
|
-
cache_ttl_seconds = cache_ttl_hours *
|
|
573
|
+
cache_ttl_seconds = cache_ttl_hours * constants.SECONDS_PER_HOUR
|
|
573
574
|
|
|
574
575
|
# Validate policies using registry
|
|
575
576
|
async with AWSServiceFetcher(
|
|
@@ -31,6 +31,7 @@ from collections.abc import Generator
|
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
|
|
33
33
|
import yaml
|
|
34
|
+
from pydantic import ValidationError
|
|
34
35
|
|
|
35
36
|
from iam_validator.core.models import IAMPolicy
|
|
36
37
|
|
|
@@ -53,6 +54,8 @@ class PolicyLoader:
|
|
|
53
54
|
"""
|
|
54
55
|
self.loaded_policies: list[tuple[str, IAMPolicy]] = []
|
|
55
56
|
self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
|
|
57
|
+
# Track parsing/validation errors for reporting
|
|
58
|
+
self.parsing_errors: list[tuple[str, str]] = [] # (file_path, error_message)
|
|
56
59
|
|
|
57
60
|
@staticmethod
|
|
58
61
|
def _find_statement_line_numbers(file_content: str) -> list[int]:
|
|
@@ -142,7 +145,7 @@ class PolicyLoader:
|
|
|
142
145
|
return False
|
|
143
146
|
return True
|
|
144
147
|
except OSError as e:
|
|
145
|
-
logger.error(
|
|
148
|
+
logger.error("Failed to check file size for %s: %s", path, e)
|
|
146
149
|
return False
|
|
147
150
|
|
|
148
151
|
def load_from_file(self, file_path: str) -> IAMPolicy | None:
|
|
@@ -157,11 +160,11 @@ class PolicyLoader:
|
|
|
157
160
|
path = Path(file_path)
|
|
158
161
|
|
|
159
162
|
if not path.exists():
|
|
160
|
-
logger.error(
|
|
163
|
+
logger.error("File not found: %s", file_path)
|
|
161
164
|
return None
|
|
162
165
|
|
|
163
166
|
if not path.is_file():
|
|
164
|
-
logger.error(
|
|
167
|
+
logger.error("Not a file: %s", file_path)
|
|
165
168
|
return None
|
|
166
169
|
|
|
167
170
|
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
|
@@ -197,17 +200,48 @@ class PolicyLoader:
|
|
|
197
200
|
if idx < len(statement_line_numbers):
|
|
198
201
|
statement.line_number = statement_line_numbers[idx]
|
|
199
202
|
|
|
200
|
-
logger.info(
|
|
203
|
+
logger.info("Successfully loaded policy from %s", file_path)
|
|
201
204
|
return policy
|
|
202
205
|
|
|
203
206
|
except json.JSONDecodeError as e:
|
|
204
|
-
|
|
207
|
+
error_msg = f"Invalid JSON: {e}"
|
|
208
|
+
logger.error("Invalid JSON in %s: %s", file_path, e)
|
|
209
|
+
self.parsing_errors.append((file_path, error_msg))
|
|
205
210
|
return None
|
|
206
211
|
except yaml.YAMLError as e:
|
|
207
|
-
|
|
212
|
+
error_msg = f"Invalid YAML: {e}"
|
|
213
|
+
logger.error("Invalid YAML in %s: %s", file_path, e)
|
|
214
|
+
self.parsing_errors.append((file_path, error_msg))
|
|
215
|
+
return None
|
|
216
|
+
except ValidationError as e:
|
|
217
|
+
# Handle Pydantic validation errors with helpful messages
|
|
218
|
+
error_messages = []
|
|
219
|
+
for error in e.errors():
|
|
220
|
+
loc = ".".join(str(x) for x in error["loc"])
|
|
221
|
+
error_type = error["type"]
|
|
222
|
+
|
|
223
|
+
# Provide user-friendly messages for common errors
|
|
224
|
+
if error_type == "extra_forbidden":
|
|
225
|
+
# Extract the field name that has a typo
|
|
226
|
+
field_name = error["loc"][-1] if error["loc"] else "unknown"
|
|
227
|
+
error_messages.append(
|
|
228
|
+
f"Unknown field '{field_name}' at {loc}. "
|
|
229
|
+
f"This might be a typo. Did you mean 'Condition', 'Action', or 'Resource'?"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
error_messages.append(f"{loc}: {error['msg']}")
|
|
233
|
+
|
|
234
|
+
error_summary = "\n ".join(error_messages)
|
|
235
|
+
logger.error(
|
|
236
|
+
"Policy validation failed for %s:\n %s",
|
|
237
|
+
file_path,
|
|
238
|
+
error_summary,
|
|
239
|
+
)
|
|
240
|
+
# Track parsing error for GitHub reporting
|
|
241
|
+
self.parsing_errors.append((file_path, error_summary))
|
|
208
242
|
return None
|
|
209
243
|
except Exception as e:
|
|
210
|
-
logger.error(
|
|
244
|
+
logger.error("Failed to load policy from %s: %s", file_path, e)
|
|
211
245
|
return None
|
|
212
246
|
|
|
213
247
|
def load_from_directory(
|
|
@@ -225,11 +259,11 @@ class PolicyLoader:
|
|
|
225
259
|
path = Path(directory_path)
|
|
226
260
|
|
|
227
261
|
if not path.exists():
|
|
228
|
-
logger.error(
|
|
262
|
+
logger.error("Directory not found: %s", directory_path)
|
|
229
263
|
return []
|
|
230
264
|
|
|
231
265
|
if not path.is_dir():
|
|
232
|
-
logger.error(
|
|
266
|
+
logger.error("Not a directory: %s", directory_path)
|
|
233
267
|
return []
|
|
234
268
|
|
|
235
269
|
policies: list[tuple[str, IAMPolicy]] = []
|
|
@@ -241,7 +275,7 @@ class PolicyLoader:
|
|
|
241
275
|
if policy:
|
|
242
276
|
policies.append((str(file_path), policy))
|
|
243
277
|
|
|
244
|
-
logger.info(
|
|
278
|
+
logger.info("Loaded %d policies from %s", len(policies), directory_path)
|
|
245
279
|
return policies
|
|
246
280
|
|
|
247
281
|
def load_from_path(self, path: str, recursive: bool = True) -> list[tuple[str, IAMPolicy]]:
|
|
@@ -262,7 +296,7 @@ class PolicyLoader:
|
|
|
262
296
|
elif path_obj.is_dir():
|
|
263
297
|
return self.load_from_directory(path, recursive)
|
|
264
298
|
else:
|
|
265
|
-
logger.error(
|
|
299
|
+
logger.error("Path not found: %s", path)
|
|
266
300
|
return []
|
|
267
301
|
|
|
268
302
|
def load_from_paths(
|
|
@@ -283,7 +317,7 @@ class PolicyLoader:
|
|
|
283
317
|
policies = self.load_from_path(path.strip(), recursive)
|
|
284
318
|
all_policies.extend(policies)
|
|
285
319
|
|
|
286
|
-
logger.info(
|
|
320
|
+
logger.info("Loaded %d total policies from %d path(s)", len(all_policies), len(paths))
|
|
287
321
|
return all_policies
|
|
288
322
|
|
|
289
323
|
def _get_policy_files(self, path: str, recursive: bool = True) -> Generator[Path, None, None]:
|
|
@@ -310,7 +344,7 @@ class PolicyLoader:
|
|
|
310
344
|
if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
|
|
311
345
|
yield file_path
|
|
312
346
|
else:
|
|
313
|
-
logger.error(
|
|
347
|
+
logger.error("Path not found: %s", path)
|
|
314
348
|
|
|
315
349
|
def stream_from_path(
|
|
316
350
|
self, path: str, recursive: bool = True
|
|
@@ -391,6 +425,29 @@ class PolicyLoader:
|
|
|
391
425
|
policy = IAMPolicy.model_validate(data)
|
|
392
426
|
logger.info("Successfully parsed policy from string")
|
|
393
427
|
return policy
|
|
428
|
+
except json.JSONDecodeError as e:
|
|
429
|
+
logger.error("Invalid JSON: %s", e)
|
|
430
|
+
return None
|
|
431
|
+
except ValidationError as e:
|
|
432
|
+
# Handle Pydantic validation errors with helpful messages
|
|
433
|
+
error_messages = []
|
|
434
|
+
for error in e.errors():
|
|
435
|
+
loc = ".".join(str(x) for x in error["loc"])
|
|
436
|
+
error_type = error["type"]
|
|
437
|
+
|
|
438
|
+
# Provide user-friendly messages for common errors
|
|
439
|
+
if error_type == "extra_forbidden":
|
|
440
|
+
# Extract the field name that has a typo
|
|
441
|
+
field_name = error["loc"][-1] if error["loc"] else "unknown"
|
|
442
|
+
error_messages.append(
|
|
443
|
+
f"Unknown field '{field_name}' at {loc}. "
|
|
444
|
+
f"This might be a typo. Did you mean 'Condition', 'Action', or 'Resource'?"
|
|
445
|
+
)
|
|
446
|
+
else:
|
|
447
|
+
error_messages.append(f"{loc}: {error['msg']}")
|
|
448
|
+
|
|
449
|
+
logger.error("Policy validation failed:\n %s", "\n ".join(error_messages))
|
|
450
|
+
return None
|
|
394
451
|
except Exception as e:
|
|
395
|
-
logger.error(
|
|
452
|
+
logger.error("Failed to parse policy string: %s", e)
|
|
396
453
|
return None
|