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.
- iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/config/config_loader.py +472 -0
- iam_validator/core/config/defaults.py +523 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/constants.py +74 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +440 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""IAM Policy Loader Module.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to load and parse IAM policy documents
|
|
4
|
+
from various file formats (JSON, YAML) and directories.
|
|
5
|
+
|
|
6
|
+
The loader supports both eager loading (load all at once) and streaming
|
|
7
|
+
(process one file at a time) to optimize memory usage.
|
|
8
|
+
|
|
9
|
+
Example usage:
|
|
10
|
+
loader = PolicyLoader()
|
|
11
|
+
|
|
12
|
+
# Eager loading (loads all files into memory)
|
|
13
|
+
policy = loader.load_from_file("policy.json")
|
|
14
|
+
policies = loader.load_from_directory("./policies/", recursive=True)
|
|
15
|
+
policies = loader.load_from_path("./policies/", recursive=False)
|
|
16
|
+
|
|
17
|
+
# Streaming (memory-efficient, processes one file at a time)
|
|
18
|
+
for file_path, policy in loader.stream_from_path("./policies/"):
|
|
19
|
+
# Process each policy immediately
|
|
20
|
+
validate_and_report(file_path, policy)
|
|
21
|
+
|
|
22
|
+
# Batch processing (configurable batch size)
|
|
23
|
+
for batch in loader.batch_from_paths(["./policies/"], batch_size=10):
|
|
24
|
+
# Process batch of up to 10 policies
|
|
25
|
+
validate_batch(batch)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
from collections.abc import Generator
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
from iam_validator.core.models import IAMPolicy
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PolicyLoader:
|
|
41
|
+
"""Loads and parses IAM policy documents from files.
|
|
42
|
+
|
|
43
|
+
Supports both eager loading and streaming for memory efficiency.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
SUPPORTED_EXTENSIONS = {".json", ".yaml", ".yml"}
|
|
47
|
+
|
|
48
|
+
def __init__(self, max_file_size_mb: int = 100) -> None:
|
|
49
|
+
"""Initialize the policy loader.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
max_file_size_mb: Maximum file size in MB to load (default: 100MB)
|
|
53
|
+
"""
|
|
54
|
+
self.loaded_policies: list[tuple[str, IAMPolicy]] = []
|
|
55
|
+
self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _find_statement_line_numbers(file_content: str) -> list[int]:
|
|
59
|
+
"""Find line numbers for each statement in a JSON policy file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
file_content: Raw content of the policy file
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of line numbers (1-indexed) for each statement's Sid or opening brace
|
|
66
|
+
"""
|
|
67
|
+
lines = file_content.split("\n")
|
|
68
|
+
statement_lines = []
|
|
69
|
+
in_statement_array = False
|
|
70
|
+
brace_depth = 0
|
|
71
|
+
statement_start_line = None
|
|
72
|
+
current_statement_first_field = None
|
|
73
|
+
|
|
74
|
+
for line_num, line in enumerate(lines, start=1):
|
|
75
|
+
# Look for "Statement" array
|
|
76
|
+
if '"Statement"' in line or "'Statement'" in line:
|
|
77
|
+
in_statement_array = True
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if not in_statement_array:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Track opening braces for statement objects
|
|
84
|
+
for char in line:
|
|
85
|
+
if char == "{":
|
|
86
|
+
if brace_depth == 0 and statement_start_line is None:
|
|
87
|
+
# Found the start of a statement object
|
|
88
|
+
statement_start_line = line_num
|
|
89
|
+
current_statement_first_field = None
|
|
90
|
+
brace_depth += 1
|
|
91
|
+
elif char == "}":
|
|
92
|
+
brace_depth -= 1
|
|
93
|
+
if brace_depth == 0 and statement_start_line is not None:
|
|
94
|
+
# Completed a statement object
|
|
95
|
+
# Use first field line if found, otherwise use opening brace
|
|
96
|
+
statement_lines.append(
|
|
97
|
+
current_statement_first_field or statement_start_line
|
|
98
|
+
)
|
|
99
|
+
statement_start_line = None
|
|
100
|
+
current_statement_first_field = None
|
|
101
|
+
elif char == "]" and brace_depth == 0:
|
|
102
|
+
# End of Statement array
|
|
103
|
+
in_statement_array = False
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
# Track first field in statement (usually Sid, Effect, or Action)
|
|
107
|
+
if (
|
|
108
|
+
in_statement_array
|
|
109
|
+
and brace_depth == 1
|
|
110
|
+
and current_statement_first_field is None
|
|
111
|
+
and statement_start_line is not None
|
|
112
|
+
):
|
|
113
|
+
stripped = line.strip()
|
|
114
|
+
# Look for first JSON field (e.g., "Sid":, "Effect":, "Action":)
|
|
115
|
+
if (
|
|
116
|
+
stripped
|
|
117
|
+
and stripped[0] == '"'
|
|
118
|
+
and ":" in stripped
|
|
119
|
+
and not stripped.startswith('"{')
|
|
120
|
+
):
|
|
121
|
+
current_statement_first_field = line_num
|
|
122
|
+
|
|
123
|
+
return statement_lines
|
|
124
|
+
|
|
125
|
+
def _check_file_size(self, path: Path) -> bool:
|
|
126
|
+
"""Check if file size is within limits.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path to the file
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if file size is acceptable, False otherwise
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
file_size = path.stat().st_size
|
|
136
|
+
if file_size > self.max_file_size_bytes:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"File {path} exceeds maximum size "
|
|
139
|
+
f"({file_size / 1024 / 1024:.2f}MB > "
|
|
140
|
+
f"{self.max_file_size_bytes / 1024 / 1024:.2f}MB). Skipping."
|
|
141
|
+
)
|
|
142
|
+
return False
|
|
143
|
+
return True
|
|
144
|
+
except OSError as e:
|
|
145
|
+
logger.error(f"Failed to check file size for {path}: {e}")
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def load_from_file(self, file_path: str) -> IAMPolicy | None:
|
|
149
|
+
"""Load a single IAM policy from a file.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
file_path: Path to the policy file
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Parsed IAMPolicy or None if loading fails
|
|
156
|
+
"""
|
|
157
|
+
path = Path(file_path)
|
|
158
|
+
|
|
159
|
+
if not path.exists():
|
|
160
|
+
logger.error(f"File not found: {file_path}")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if not path.is_file():
|
|
164
|
+
logger.error(f"Not a file: {file_path}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
|
|
168
|
+
logger.warning(
|
|
169
|
+
f"Unsupported file extension: {path.suffix}. "
|
|
170
|
+
f"Supported: {', '.join(self.SUPPORTED_EXTENSIONS)}"
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Check file size before loading
|
|
175
|
+
if not self._check_file_size(path):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
with open(path, encoding="utf-8") as f:
|
|
180
|
+
file_content = f.read()
|
|
181
|
+
|
|
182
|
+
# Parse line numbers for JSON files
|
|
183
|
+
statement_line_numbers = []
|
|
184
|
+
if path.suffix.lower() == ".json":
|
|
185
|
+
statement_line_numbers = self._find_statement_line_numbers(file_content)
|
|
186
|
+
data = json.loads(file_content)
|
|
187
|
+
else: # .yaml or .yml
|
|
188
|
+
data = yaml.safe_load(file_content)
|
|
189
|
+
# TODO: Add YAML line number tracking if needed
|
|
190
|
+
|
|
191
|
+
# Validate and parse the policy
|
|
192
|
+
policy = IAMPolicy.model_validate(data)
|
|
193
|
+
|
|
194
|
+
# Attach line numbers to statements
|
|
195
|
+
if statement_line_numbers:
|
|
196
|
+
for idx, statement in enumerate(policy.statement):
|
|
197
|
+
if idx < len(statement_line_numbers):
|
|
198
|
+
statement.line_number = statement_line_numbers[idx]
|
|
199
|
+
|
|
200
|
+
logger.info(f"Successfully loaded policy from {file_path}")
|
|
201
|
+
return policy
|
|
202
|
+
|
|
203
|
+
except json.JSONDecodeError as e:
|
|
204
|
+
logger.error(f"Invalid JSON in {file_path}: {e}")
|
|
205
|
+
return None
|
|
206
|
+
except yaml.YAMLError as e:
|
|
207
|
+
logger.error(f"Invalid YAML in {file_path}: {e}")
|
|
208
|
+
return None
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Failed to load policy from {file_path}: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def load_from_directory(
|
|
214
|
+
self, directory_path: str, recursive: bool = True
|
|
215
|
+
) -> list[tuple[str, IAMPolicy]]:
|
|
216
|
+
"""Load all IAM policies from a directory.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
directory_path: Path to the directory
|
|
220
|
+
recursive: Whether to search subdirectories
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of tuples (file_path, policy)
|
|
224
|
+
"""
|
|
225
|
+
path = Path(directory_path)
|
|
226
|
+
|
|
227
|
+
if not path.exists():
|
|
228
|
+
logger.error(f"Directory not found: {directory_path}")
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
if not path.is_dir():
|
|
232
|
+
logger.error(f"Not a directory: {directory_path}")
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
policies: list[tuple[str, IAMPolicy]] = []
|
|
236
|
+
pattern = "**/*" if recursive else "*"
|
|
237
|
+
|
|
238
|
+
for file_path in path.glob(pattern):
|
|
239
|
+
if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
|
|
240
|
+
policy = self.load_from_file(str(file_path))
|
|
241
|
+
if policy:
|
|
242
|
+
policies.append((str(file_path), policy))
|
|
243
|
+
|
|
244
|
+
logger.info(f"Loaded {len(policies)} policies from {directory_path}")
|
|
245
|
+
return policies
|
|
246
|
+
|
|
247
|
+
def load_from_path(self, path: str, recursive: bool = True) -> list[tuple[str, IAMPolicy]]:
|
|
248
|
+
"""Load IAM policies from a file or directory.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
path: Path to file or directory
|
|
252
|
+
recursive: Whether to search subdirectories (only applies to directories)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of tuples (file_path, policy)
|
|
256
|
+
"""
|
|
257
|
+
path_obj = Path(path)
|
|
258
|
+
|
|
259
|
+
if path_obj.is_file():
|
|
260
|
+
policy = self.load_from_file(path)
|
|
261
|
+
return [(path, policy)] if policy else []
|
|
262
|
+
elif path_obj.is_dir():
|
|
263
|
+
return self.load_from_directory(path, recursive)
|
|
264
|
+
else:
|
|
265
|
+
logger.error(f"Path not found: {path}")
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
def load_from_paths(
|
|
269
|
+
self, paths: list[str], recursive: bool = True
|
|
270
|
+
) -> list[tuple[str, IAMPolicy]]:
|
|
271
|
+
"""Load IAM policies from multiple files or directories.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
paths: List of paths to files or directories
|
|
275
|
+
recursive: Whether to search subdirectories (only applies to directories)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of tuples (file_path, policy) from all paths combined
|
|
279
|
+
"""
|
|
280
|
+
all_policies: list[tuple[str, IAMPolicy]] = []
|
|
281
|
+
|
|
282
|
+
for path in paths:
|
|
283
|
+
policies = self.load_from_path(path.strip(), recursive)
|
|
284
|
+
all_policies.extend(policies)
|
|
285
|
+
|
|
286
|
+
logger.info(f"Loaded {len(all_policies)} total policies from {len(paths)} path(s)")
|
|
287
|
+
return all_policies
|
|
288
|
+
|
|
289
|
+
def _get_policy_files(self, path: str, recursive: bool = True) -> Generator[Path, None, None]:
|
|
290
|
+
"""Get all policy files from a path (file or directory).
|
|
291
|
+
|
|
292
|
+
This is a generator that yields file paths without loading them,
|
|
293
|
+
enabling memory-efficient iteration.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
path: Path to file or directory
|
|
297
|
+
recursive: Whether to search subdirectories
|
|
298
|
+
|
|
299
|
+
Yields:
|
|
300
|
+
Path objects for policy files
|
|
301
|
+
"""
|
|
302
|
+
path_obj = Path(path)
|
|
303
|
+
|
|
304
|
+
if path_obj.is_file():
|
|
305
|
+
if path_obj.suffix.lower() in self.SUPPORTED_EXTENSIONS:
|
|
306
|
+
yield path_obj
|
|
307
|
+
elif path_obj.is_dir():
|
|
308
|
+
pattern = "**/*" if recursive else "*"
|
|
309
|
+
for file_path in path_obj.glob(pattern):
|
|
310
|
+
if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
|
|
311
|
+
yield file_path
|
|
312
|
+
else:
|
|
313
|
+
logger.error(f"Path not found: {path}")
|
|
314
|
+
|
|
315
|
+
def stream_from_path(
|
|
316
|
+
self, path: str, recursive: bool = True
|
|
317
|
+
) -> Generator[tuple[str, IAMPolicy], None, None]:
|
|
318
|
+
"""Stream IAM policies from a file or directory one at a time.
|
|
319
|
+
|
|
320
|
+
This is a memory-efficient alternative to load_from_path that yields
|
|
321
|
+
policies one at a time instead of loading all into memory.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
path: Path to file or directory
|
|
325
|
+
recursive: Whether to search subdirectories
|
|
326
|
+
|
|
327
|
+
Yields:
|
|
328
|
+
Tuples of (file_path, policy) for each successfully loaded policy
|
|
329
|
+
"""
|
|
330
|
+
for file_path in self._get_policy_files(path, recursive):
|
|
331
|
+
policy = self.load_from_file(str(file_path))
|
|
332
|
+
if policy:
|
|
333
|
+
yield (str(file_path), policy)
|
|
334
|
+
|
|
335
|
+
def stream_from_paths(
|
|
336
|
+
self, paths: list[str], recursive: bool = True
|
|
337
|
+
) -> Generator[tuple[str, IAMPolicy], None, None]:
|
|
338
|
+
"""Stream IAM policies from multiple paths one at a time.
|
|
339
|
+
|
|
340
|
+
This is a memory-efficient alternative to load_from_paths that yields
|
|
341
|
+
policies one at a time instead of loading all into memory.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
paths: List of paths to files or directories
|
|
345
|
+
recursive: Whether to search subdirectories
|
|
346
|
+
|
|
347
|
+
Yields:
|
|
348
|
+
Tuples of (file_path, policy) for each successfully loaded policy
|
|
349
|
+
"""
|
|
350
|
+
for path in paths:
|
|
351
|
+
yield from self.stream_from_path(path.strip(), recursive)
|
|
352
|
+
|
|
353
|
+
def batch_from_paths(
|
|
354
|
+
self, paths: list[str], batch_size: int = 10, recursive: bool = True
|
|
355
|
+
) -> Generator[list[tuple[str, IAMPolicy]], None, None]:
|
|
356
|
+
"""Load policies in batches for balanced memory usage and performance.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
paths: List of paths to files or directories
|
|
360
|
+
batch_size: Number of policies per batch (default: 10)
|
|
361
|
+
recursive: Whether to search subdirectories
|
|
362
|
+
|
|
363
|
+
Yields:
|
|
364
|
+
Lists of (file_path, policy) tuples, up to batch_size per list
|
|
365
|
+
"""
|
|
366
|
+
batch: list[tuple[str, IAMPolicy]] = []
|
|
367
|
+
|
|
368
|
+
for file_path, policy in self.stream_from_paths(paths, recursive):
|
|
369
|
+
batch.append((file_path, policy))
|
|
370
|
+
|
|
371
|
+
if len(batch) >= batch_size:
|
|
372
|
+
yield batch
|
|
373
|
+
batch = []
|
|
374
|
+
|
|
375
|
+
# Yield remaining policies
|
|
376
|
+
if batch:
|
|
377
|
+
yield batch
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def parse_policy_string(policy_json: str) -> IAMPolicy | None:
|
|
381
|
+
"""Parse an IAM policy from a JSON string.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
policy_json: JSON string containing the policy
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Parsed IAMPolicy or None if parsing fails
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
data = json.loads(policy_json)
|
|
391
|
+
policy = IAMPolicy.model_validate(data)
|
|
392
|
+
logger.info("Successfully parsed policy from string")
|
|
393
|
+
return policy
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f"Failed to parse policy string: {e}")
|
|
396
|
+
return None
|