iam-policy-validator 1.0.3__py3-none-any.whl → 1.1.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.0.3.dist-info → iam_policy_validator-1.1.0.dist-info}/METADATA +210 -436
- {iam_policy_validator-1.0.3.dist-info → iam_policy_validator-1.1.0.dist-info}/RECORD +20 -18
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +112 -28
- iam_validator/checks/security_best_practices.py +103 -12
- iam_validator/commands/validate.py +7 -5
- iam_validator/core/cli.py +26 -9
- iam_validator/core/config_loader.py +39 -3
- iam_validator/core/defaults.py +304 -0
- iam_validator/core/formatters/__init__.py +2 -0
- iam_validator/core/formatters/console.py +44 -7
- iam_validator/core/formatters/csv.py +7 -2
- iam_validator/core/formatters/enhanced.py +428 -0
- iam_validator/core/formatters/html.py +127 -37
- iam_validator/core/formatters/markdown.py +10 -2
- iam_validator/core/models.py +30 -6
- iam_validator/core/report.py +104 -25
- {iam_policy_validator-1.0.3.dist-info → iam_policy_validator-1.1.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.0.3.dist-info → iam_policy_validator-1.1.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.0.3.dist-info → iam_policy_validator-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
|
|
2
2
|
iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
|
|
3
|
-
iam_validator/__version__.py,sha256=
|
|
3
|
+
iam_validator/__version__.py,sha256=YOIURWR5ocuvaQTQgwFi1XjHm_ifJDzicMOQSJZqmZc,206
|
|
4
4
|
iam_validator/checks/__init__.py,sha256=q-_rIYGZJMjsiHK-R_3CbSUCBVGN5e137LPNDnMRZmw,841
|
|
5
|
-
iam_validator/checks/action_condition_enforcement.py,sha256=
|
|
5
|
+
iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
|
|
6
6
|
iam_validator/checks/action_validation.py,sha256=KbUw1SV-2nN-HtLlj3zrE6sdd0z8iAF0ubqz35Vwb7c,6921
|
|
7
7
|
iam_validator/checks/condition_key_validation.py,sha256=bc4LQ8IRKyt0RquaQvQvVjmeJnuOUAFRL8xdduLPa_U,2661
|
|
8
8
|
iam_validator/checks/policy_size.py,sha256=4cvZiWRJXGuvYo8PRcdD1Py_ZL8Xw0lOJfXTs6EX-_I,5753
|
|
9
9
|
iam_validator/checks/resource_validation.py,sha256=AEIoiR6AKYLuVaA8ne3QE5qy6NCMDe98_2JAiwE9-JU,4261
|
|
10
|
-
iam_validator/checks/security_best_practices.py,sha256=
|
|
10
|
+
iam_validator/checks/security_best_practices.py,sha256=OCAtbsO9HEK97DVPPnm-hJDQtf-ATlnwa1LLshweZDk,32045
|
|
11
11
|
iam_validator/checks/sid_uniqueness.py,sha256=7S8RtVJgYPTKgr7gSEmxgT0oIGkSoXN6iu0ALHbcSfw,5015
|
|
12
12
|
iam_validator/commands/__init__.py,sha256=zuhECuz-1Us5hBNAJtdMae8LaYn1eNeoYPBmQPMwI94,357
|
|
13
13
|
iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
|
|
14
14
|
iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
|
|
15
15
|
iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
|
|
16
|
-
iam_validator/commands/validate.py,sha256=
|
|
16
|
+
iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
|
|
17
17
|
iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
|
|
18
18
|
iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
|
|
19
19
|
iam_validator/core/access_analyzer_report.py,sha256=iTIFKul6zQZd2qBg8V6zaDNPMKF8D_XDSX6pJwXFVYY,24791
|
|
20
20
|
iam_validator/core/aws_fetcher.py,sha256=fUCIMItIWmrbgsoVCz_9Oe5k3SjuXBlNBVwQ60IWwns,25492
|
|
21
21
|
iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
|
|
22
22
|
iam_validator/core/check_registry.py,sha256=wXg4Yw5LJ-rAVLiPUIJOtw8Y49Q1PY00Zbu37LzyjHY,15477
|
|
23
|
-
iam_validator/core/cli.py,sha256=
|
|
24
|
-
iam_validator/core/config_loader.py,sha256=
|
|
25
|
-
iam_validator/core/
|
|
23
|
+
iam_validator/core/cli.py,sha256=5UHsHS8o7Fkag4d6MNaaqjCFSGu8evCIbtpa81591lE,3831
|
|
24
|
+
iam_validator/core/config_loader.py,sha256=6Px_JEzk8WU6g8KXaSLme3x8qZXT9oppxUVXYyDkboY,16425
|
|
25
|
+
iam_validator/core/defaults.py,sha256=JzuDYNQGERDtX9S8E5grr4KZmooSephJW8KRplT34L8,10956
|
|
26
|
+
iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
|
|
26
27
|
iam_validator/core/policy_checks.py,sha256=xK5CntsEKVDgN27uIdQ92jCL97t7eBqOk0SChWU9cgw,23872
|
|
27
28
|
iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
|
|
28
29
|
iam_validator/core/pr_commenter.py,sha256=TOhVXKTFcRHQ9EVuShXQcKXn9aNjB1mU6FnR2jvltmw,10581
|
|
29
|
-
iam_validator/core/report.py,sha256=
|
|
30
|
-
iam_validator/core/formatters/__init__.py,sha256=
|
|
30
|
+
iam_validator/core/report.py,sha256=aRM1mYlDjkPmQrUZtcq2akbviLPVTSa8q_mH7XyuuK4,32897
|
|
31
|
+
iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
|
|
31
32
|
iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
|
|
32
|
-
iam_validator/core/formatters/console.py,sha256=
|
|
33
|
-
iam_validator/core/formatters/csv.py,sha256=
|
|
34
|
-
iam_validator/core/formatters/
|
|
33
|
+
iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
|
|
34
|
+
iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
|
|
35
|
+
iam_validator/core/formatters/enhanced.py,sha256=6hRiATRpH6Osqf_y6C58VN48L47AXYP3Dm9R90VMGs8,16432
|
|
36
|
+
iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
|
|
35
37
|
iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
|
|
36
|
-
iam_validator/core/formatters/markdown.py,sha256=
|
|
38
|
+
iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
|
|
37
39
|
iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkSpy1MM8Vi4,7200
|
|
38
40
|
iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
|
|
39
41
|
iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
|
|
40
42
|
iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
|
|
41
|
-
iam_policy_validator-1.0.
|
|
42
|
-
iam_policy_validator-1.0.
|
|
43
|
-
iam_policy_validator-1.0.
|
|
44
|
-
iam_policy_validator-1.0.
|
|
45
|
-
iam_policy_validator-1.0.
|
|
43
|
+
iam_policy_validator-1.1.0.dist-info/METADATA,sha256=a_cegBs1dAS4y_ByXskcvgXY3CNsqC-7frERfdQR27k,25144
|
|
44
|
+
iam_policy_validator-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
45
|
+
iam_policy_validator-1.1.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
46
|
+
iam_policy_validator-1.1.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
47
|
+
iam_policy_validator-1.1.0.dist-info/RECORD,,
|
iam_validator/__version__.py
CHANGED
|
@@ -139,8 +139,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
139
139
|
# Check each requirement rule
|
|
140
140
|
for requirement in action_condition_requirements:
|
|
141
141
|
# Check if this requirement applies to the statement's actions
|
|
142
|
-
actions_match, matching_actions = self._check_action_match(
|
|
143
|
-
statement_actions, requirement
|
|
142
|
+
actions_match, matching_actions = await self._check_action_match(
|
|
143
|
+
statement_actions, requirement, fetcher
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
if not actions_match:
|
|
@@ -186,8 +186,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
186
186
|
|
|
187
187
|
return issues
|
|
188
188
|
|
|
189
|
-
def _check_action_match(
|
|
190
|
-
self, statement_actions: list[str], requirement: dict[str, Any]
|
|
189
|
+
async def _check_action_match(
|
|
190
|
+
self, statement_actions: list[str], requirement: dict[str, Any], fetcher: AWSServiceFetcher
|
|
191
191
|
) -> tuple[bool, list[str]]:
|
|
192
192
|
"""
|
|
193
193
|
Check if statement actions match the requirement.
|
|
@@ -208,18 +208,25 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
208
208
|
if stmt_action == "*":
|
|
209
209
|
continue
|
|
210
210
|
|
|
211
|
-
# Check
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
# Check if this statement action matches any of the required actions or patterns
|
|
212
|
+
# Use _action_matches which handles wildcards in both statement and config
|
|
213
|
+
matched = False
|
|
214
214
|
|
|
215
|
-
# Check
|
|
216
|
-
for
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
# Check against configured actions
|
|
216
|
+
for required_action in actions_config:
|
|
217
|
+
if await self._action_matches(
|
|
218
|
+
stmt_action, required_action, action_patterns, fetcher
|
|
219
|
+
):
|
|
220
|
+
matched = True
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
# If not matched by actions, check if wildcard overlaps with patterns
|
|
224
|
+
if not matched and "*" in stmt_action:
|
|
225
|
+
# For wildcards, also check pattern overlap directly
|
|
226
|
+
matched = await self._action_matches(stmt_action, "", action_patterns, fetcher)
|
|
227
|
+
|
|
228
|
+
if matched and stmt_action not in matching_actions:
|
|
229
|
+
matching_actions.append(stmt_action)
|
|
223
230
|
|
|
224
231
|
return len(matching_actions) > 0, matching_actions
|
|
225
232
|
|
|
@@ -231,20 +238,28 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
231
238
|
|
|
232
239
|
# Check all_of: ALL specified actions must be in statement
|
|
233
240
|
if all_of:
|
|
234
|
-
all_present =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
all_present = True
|
|
242
|
+
for req_action in all_of:
|
|
243
|
+
found = False
|
|
244
|
+
for stmt_action in statement_actions:
|
|
245
|
+
if await self._action_matches(
|
|
246
|
+
stmt_action, req_action, action_patterns, fetcher
|
|
247
|
+
):
|
|
248
|
+
found = True
|
|
249
|
+
break
|
|
250
|
+
if not found:
|
|
251
|
+
all_present = False
|
|
252
|
+
break
|
|
253
|
+
|
|
241
254
|
if not all_present:
|
|
242
255
|
return False, []
|
|
243
256
|
|
|
244
257
|
# Collect matching actions
|
|
245
258
|
for stmt_action in statement_actions:
|
|
246
259
|
for req_action in all_of:
|
|
247
|
-
if self._action_matches(
|
|
260
|
+
if await self._action_matches(
|
|
261
|
+
stmt_action, req_action, action_patterns, fetcher
|
|
262
|
+
):
|
|
248
263
|
if stmt_action not in matching_actions:
|
|
249
264
|
matching_actions.append(stmt_action)
|
|
250
265
|
|
|
@@ -253,7 +268,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
253
268
|
any_present = False
|
|
254
269
|
for stmt_action in statement_actions:
|
|
255
270
|
for req_action in any_of:
|
|
256
|
-
if self._action_matches(
|
|
271
|
+
if await self._action_matches(
|
|
272
|
+
stmt_action, req_action, action_patterns, fetcher
|
|
273
|
+
):
|
|
257
274
|
any_present = True
|
|
258
275
|
if stmt_action not in matching_actions:
|
|
259
276
|
matching_actions.append(stmt_action)
|
|
@@ -266,7 +283,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
266
283
|
forbidden_actions = []
|
|
267
284
|
for stmt_action in statement_actions:
|
|
268
285
|
for forbidden_action in none_of:
|
|
269
|
-
if self._action_matches(
|
|
286
|
+
if await self._action_matches(
|
|
287
|
+
stmt_action, forbidden_action, action_patterns, fetcher
|
|
288
|
+
):
|
|
270
289
|
forbidden_actions.append(stmt_action)
|
|
271
290
|
|
|
272
291
|
# If forbidden actions are found, this is a match for flagging
|
|
@@ -277,15 +296,23 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
277
296
|
|
|
278
297
|
return False, []
|
|
279
298
|
|
|
280
|
-
def _action_matches(
|
|
281
|
-
self,
|
|
299
|
+
async def _action_matches(
|
|
300
|
+
self,
|
|
301
|
+
statement_action: str,
|
|
302
|
+
required_action: str,
|
|
303
|
+
patterns: list[str],
|
|
304
|
+
fetcher: AWSServiceFetcher,
|
|
282
305
|
) -> bool:
|
|
283
306
|
"""
|
|
284
307
|
Check if a statement action matches a required action or pattern.
|
|
285
308
|
Supports:
|
|
286
309
|
- Exact matches: "s3:GetObject"
|
|
287
|
-
- AWS wildcards: "s3:*", "s3:Get*"
|
|
310
|
+
- AWS wildcards in both statement and required actions: "s3:*", "s3:Get*", "iam:Creat*"
|
|
288
311
|
- Regex patterns: "^s3:Get.*", "^iam:Delete.*"
|
|
312
|
+
|
|
313
|
+
This method handles bidirectional wildcard matching using real AWS actions from the fetcher:
|
|
314
|
+
- statement_action="iam:Create*" matches required_action="iam:CreateUser"
|
|
315
|
+
- statement_action="iam:C*" matches pattern="^iam:Create" (by checking actual AWS actions)
|
|
289
316
|
"""
|
|
290
317
|
if statement_action == "*":
|
|
291
318
|
return False
|
|
@@ -304,6 +331,63 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
304
331
|
except re.error:
|
|
305
332
|
pass
|
|
306
333
|
|
|
334
|
+
# AWS wildcard match in statement_action (e.g., "iam:Creat*" in policy)
|
|
335
|
+
# Check if this wildcard would grant access to actions matching our patterns
|
|
336
|
+
if "*" in statement_action:
|
|
337
|
+
# Convert statement wildcard to regex pattern
|
|
338
|
+
stmt_wildcard_pattern = statement_action.replace("*", ".*").replace("?", ".")
|
|
339
|
+
|
|
340
|
+
# Check if statement wildcard overlaps with required action
|
|
341
|
+
if "*" not in required_action:
|
|
342
|
+
# Required action is specific (e.g., "iam:CreateUser")
|
|
343
|
+
# Check if statement wildcard would grant it
|
|
344
|
+
try:
|
|
345
|
+
if re.match(f"^{stmt_wildcard_pattern}$", required_action):
|
|
346
|
+
return True
|
|
347
|
+
except re.error:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
# Check if statement wildcard overlaps with any of our action patterns
|
|
351
|
+
# Strategy: Use real AWS actions from the fetcher instead of hardcoded guesses
|
|
352
|
+
# For example: "iam:C*" should match pattern "^iam:Create" because:
|
|
353
|
+
# - "iam:C*" grants iam:CreateUser, iam:CreateRole, etc. (from AWS)
|
|
354
|
+
# - "^iam:Create" pattern is meant to catch iam:CreateUser, iam:CreateRole, etc.
|
|
355
|
+
# - Therefore they overlap
|
|
356
|
+
if patterns:
|
|
357
|
+
try:
|
|
358
|
+
# Parse the service from the wildcard action
|
|
359
|
+
service_prefix, _ = fetcher.parse_action(statement_action)
|
|
360
|
+
|
|
361
|
+
# Fetch the real list of actions for this service
|
|
362
|
+
service_detail = await fetcher.fetch_service_by_name(service_prefix)
|
|
363
|
+
available_actions = list(service_detail.actions.keys())
|
|
364
|
+
|
|
365
|
+
# Find which actual AWS actions the wildcard would grant
|
|
366
|
+
_, granted_actions = fetcher._match_wildcard_action(
|
|
367
|
+
statement_action.split(":", 1)[1], # Just the action part (e.g., "C*")
|
|
368
|
+
available_actions,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Check if any of the granted actions match our patterns
|
|
372
|
+
for granted_action in granted_actions:
|
|
373
|
+
full_granted_action = f"{service_prefix}:{granted_action}"
|
|
374
|
+
for pattern in patterns:
|
|
375
|
+
try:
|
|
376
|
+
if re.match(pattern, full_granted_action):
|
|
377
|
+
return True
|
|
378
|
+
except re.error:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
except (ValueError, Exception):
|
|
382
|
+
# If we can't fetch the service or parse the action, fall back to prefix matching
|
|
383
|
+
stmt_prefix = statement_action.rstrip("*")
|
|
384
|
+
for pattern in patterns:
|
|
385
|
+
try:
|
|
386
|
+
if re.match(pattern, stmt_prefix):
|
|
387
|
+
return True
|
|
388
|
+
except re.error:
|
|
389
|
+
continue
|
|
390
|
+
|
|
307
391
|
# Regex pattern match (from action_patterns config)
|
|
308
392
|
for pattern in patterns:
|
|
309
393
|
try:
|
|
@@ -91,14 +91,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
91
91
|
if self._is_sub_check_enabled(config, "wildcard_action_check"):
|
|
92
92
|
if "*" in actions:
|
|
93
93
|
severity = self._get_sub_check_severity(config, "wildcard_action_check", "warning")
|
|
94
|
+
sub_check_config = config.config.get("wildcard_action_check", {})
|
|
95
|
+
|
|
96
|
+
message = sub_check_config.get("message", "Statement allows all actions (*)")
|
|
97
|
+
suggestion_text = sub_check_config.get(
|
|
98
|
+
"suggestion", "Consider limiting to specific actions needed"
|
|
99
|
+
)
|
|
100
|
+
example = sub_check_config.get("example", "")
|
|
101
|
+
|
|
102
|
+
# Combine suggestion + example like action_condition_enforcement does
|
|
103
|
+
suggestion = (
|
|
104
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
105
|
+
)
|
|
106
|
+
|
|
94
107
|
issues.append(
|
|
95
108
|
ValidationIssue(
|
|
96
109
|
severity=severity,
|
|
97
110
|
statement_sid=statement_sid,
|
|
98
111
|
statement_index=statement_idx,
|
|
99
112
|
issue_type="overly_permissive",
|
|
100
|
-
message=
|
|
101
|
-
suggestion=
|
|
113
|
+
message=message,
|
|
114
|
+
suggestion=suggestion,
|
|
102
115
|
line_number=line_number,
|
|
103
116
|
)
|
|
104
117
|
)
|
|
@@ -109,14 +122,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
109
122
|
severity = self._get_sub_check_severity(
|
|
110
123
|
config, "wildcard_resource_check", "warning"
|
|
111
124
|
)
|
|
125
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
126
|
+
|
|
127
|
+
message = sub_check_config.get("message", "Statement applies to all resources (*)")
|
|
128
|
+
suggestion_text = sub_check_config.get(
|
|
129
|
+
"suggestion", "Consider limiting to specific resources"
|
|
130
|
+
)
|
|
131
|
+
example = sub_check_config.get("example", "")
|
|
132
|
+
|
|
133
|
+
# Combine suggestion + example
|
|
134
|
+
suggestion = (
|
|
135
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
136
|
+
)
|
|
137
|
+
|
|
112
138
|
issues.append(
|
|
113
139
|
ValidationIssue(
|
|
114
140
|
severity=severity,
|
|
115
141
|
statement_sid=statement_sid,
|
|
116
142
|
statement_index=statement_idx,
|
|
117
143
|
issue_type="overly_permissive",
|
|
118
|
-
message=
|
|
119
|
-
suggestion=
|
|
144
|
+
message=message,
|
|
145
|
+
suggestion=suggestion,
|
|
120
146
|
line_number=line_number,
|
|
121
147
|
)
|
|
122
148
|
)
|
|
@@ -125,14 +151,31 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
125
151
|
if self._is_sub_check_enabled(config, "full_wildcard_check"):
|
|
126
152
|
if "*" in actions and "*" in resources:
|
|
127
153
|
severity = self._get_sub_check_severity(config, "full_wildcard_check", "error")
|
|
154
|
+
sub_check_config = config.config.get("full_wildcard_check", {})
|
|
155
|
+
|
|
156
|
+
message = sub_check_config.get(
|
|
157
|
+
"message",
|
|
158
|
+
"Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
159
|
+
)
|
|
160
|
+
suggestion_text = sub_check_config.get(
|
|
161
|
+
"suggestion",
|
|
162
|
+
"This grants full administrative access. Restrict to specific actions and resources.",
|
|
163
|
+
)
|
|
164
|
+
example = sub_check_config.get("example", "")
|
|
165
|
+
|
|
166
|
+
# Combine suggestion + example
|
|
167
|
+
suggestion = (
|
|
168
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
169
|
+
)
|
|
170
|
+
|
|
128
171
|
issues.append(
|
|
129
172
|
ValidationIssue(
|
|
130
173
|
severity=severity,
|
|
131
174
|
statement_sid=statement_sid,
|
|
132
175
|
statement_index=statement_idx,
|
|
133
176
|
issue_type="security_risk",
|
|
134
|
-
message=
|
|
135
|
-
suggestion=
|
|
177
|
+
message=message,
|
|
178
|
+
suggestion=suggestion,
|
|
136
179
|
line_number=line_number,
|
|
137
180
|
)
|
|
138
181
|
)
|
|
@@ -155,15 +198,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
155
198
|
severity = self._get_sub_check_severity(
|
|
156
199
|
config, "service_wildcard_check", "warning"
|
|
157
200
|
)
|
|
201
|
+
sub_check_config = config.config.get("service_wildcard_check", {})
|
|
202
|
+
|
|
203
|
+
# Get message template and replace placeholders
|
|
204
|
+
message_template = sub_check_config.get(
|
|
205
|
+
"message",
|
|
206
|
+
"Service-level wildcard '{action}' grants all permissions for {service} service",
|
|
207
|
+
)
|
|
208
|
+
suggestion_template = sub_check_config.get(
|
|
209
|
+
"suggestion",
|
|
210
|
+
"Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
|
|
211
|
+
)
|
|
212
|
+
example_template = sub_check_config.get("example", "")
|
|
213
|
+
|
|
214
|
+
message = message_template.format(action=action, service=service)
|
|
215
|
+
suggestion_text = suggestion_template.format(action=action, service=service)
|
|
216
|
+
example = (
|
|
217
|
+
example_template.format(action=action, service=service)
|
|
218
|
+
if example_template
|
|
219
|
+
else ""
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Combine suggestion + example
|
|
223
|
+
suggestion = (
|
|
224
|
+
f"{suggestion_text}\nExample:\n{example}"
|
|
225
|
+
if example
|
|
226
|
+
else suggestion_text
|
|
227
|
+
)
|
|
228
|
+
|
|
158
229
|
issues.append(
|
|
159
230
|
ValidationIssue(
|
|
160
231
|
severity=severity,
|
|
161
232
|
statement_sid=statement_sid,
|
|
162
233
|
statement_index=statement_idx,
|
|
163
234
|
issue_type="overly_permissive",
|
|
164
|
-
message=
|
|
235
|
+
message=message,
|
|
165
236
|
action=action,
|
|
166
|
-
suggestion=
|
|
237
|
+
suggestion=suggestion,
|
|
167
238
|
line_number=line_number,
|
|
168
239
|
)
|
|
169
240
|
)
|
|
@@ -177,13 +248,33 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
177
248
|
|
|
178
249
|
if is_sensitive and not has_conditions:
|
|
179
250
|
severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
|
|
251
|
+
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
180
252
|
|
|
181
|
-
# Create appropriate message based on matched actions
|
|
253
|
+
# Create appropriate message based on matched actions using configurable templates
|
|
182
254
|
if len(matched_actions) == 1:
|
|
183
|
-
|
|
255
|
+
message_template = sub_check_config.get(
|
|
256
|
+
"message_single",
|
|
257
|
+
"Sensitive action '{action}' should have conditions to limit when it can be used",
|
|
258
|
+
)
|
|
259
|
+
message = message_template.format(action=matched_actions[0])
|
|
184
260
|
else:
|
|
185
261
|
action_list = "', '".join(matched_actions)
|
|
186
|
-
|
|
262
|
+
message_template = sub_check_config.get(
|
|
263
|
+
"message_multiple",
|
|
264
|
+
"Sensitive actions '{actions}' should have conditions to limit when they can be used",
|
|
265
|
+
)
|
|
266
|
+
message = message_template.format(actions=action_list)
|
|
267
|
+
|
|
268
|
+
suggestion_text = sub_check_config.get(
|
|
269
|
+
"suggestion",
|
|
270
|
+
"Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
|
|
271
|
+
)
|
|
272
|
+
example = sub_check_config.get("example", "")
|
|
273
|
+
|
|
274
|
+
# Combine suggestion + example
|
|
275
|
+
suggestion = (
|
|
276
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
277
|
+
)
|
|
187
278
|
|
|
188
279
|
issues.append(
|
|
189
280
|
ValidationIssue(
|
|
@@ -193,7 +284,7 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
193
284
|
issue_type="missing_condition",
|
|
194
285
|
message=message,
|
|
195
286
|
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
196
|
-
suggestion=
|
|
287
|
+
suggestion=suggestion,
|
|
197
288
|
line_number=line_number,
|
|
198
289
|
)
|
|
199
290
|
)
|
|
@@ -59,9 +59,9 @@ Examples:
|
|
|
59
59
|
parser.add_argument(
|
|
60
60
|
"--format",
|
|
61
61
|
"-f",
|
|
62
|
-
choices=["console", "json", "markdown", "html", "csv", "sarif"],
|
|
62
|
+
choices=["console", "enhanced", "json", "markdown", "html", "csv", "sarif"],
|
|
63
63
|
default="console",
|
|
64
|
-
help="Output format (default: console)",
|
|
64
|
+
help="Output format (default: console). Use 'enhanced' for modern visual output with Rich library",
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
parser.add_argument(
|
|
@@ -177,7 +177,8 @@ Examples:
|
|
|
177
177
|
report = generator.generate_report(results)
|
|
178
178
|
|
|
179
179
|
# Output results
|
|
180
|
-
if args.format
|
|
180
|
+
if args.format is None:
|
|
181
|
+
# Default: use classic console output (direct Rich printing)
|
|
181
182
|
generator.print_console_report(report)
|
|
182
183
|
elif args.format == "json":
|
|
183
184
|
if args.output:
|
|
@@ -190,7 +191,7 @@ Examples:
|
|
|
190
191
|
else:
|
|
191
192
|
print(generator.generate_github_comment(report))
|
|
192
193
|
else:
|
|
193
|
-
# Use formatter registry for other formats (html, csv, sarif)
|
|
194
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
194
195
|
output_content = generator.format_report(report, args.format)
|
|
195
196
|
if args.output:
|
|
196
197
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
@@ -285,6 +286,7 @@ Examples:
|
|
|
285
286
|
|
|
286
287
|
# Output final results
|
|
287
288
|
if args.format == "console":
|
|
289
|
+
# Classic console output (direct Rich printing from report.py)
|
|
288
290
|
generator.print_console_report(report)
|
|
289
291
|
elif args.format == "json":
|
|
290
292
|
if args.output:
|
|
@@ -297,7 +299,7 @@ Examples:
|
|
|
297
299
|
else:
|
|
298
300
|
print(generator.generate_github_comment(report))
|
|
299
301
|
else:
|
|
300
|
-
# Use formatter registry for other formats (html, csv, sarif)
|
|
302
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
301
303
|
output_content = generator.format_report(report, args.format)
|
|
302
304
|
if args.output:
|
|
303
305
|
with open(args.output, "w", encoding="utf-8") as f:
|
iam_validator/core/cli.py
CHANGED
|
@@ -10,18 +10,24 @@ from iam_validator import __version__
|
|
|
10
10
|
from iam_validator.commands import ALL_COMMANDS
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def setup_logging(verbose: bool = False) -> None:
|
|
13
|
+
def setup_logging(log_level: str | None = None, verbose: bool = False) -> None:
|
|
14
14
|
"""Setup logging configuration.
|
|
15
15
|
|
|
16
16
|
Args:
|
|
17
|
-
|
|
17
|
+
log_level: Log level from CLI argument (debug, info, warning, error, critical)
|
|
18
|
+
verbose: Enable verbose logging (deprecated, use --log-level debug instead)
|
|
18
19
|
|
|
19
20
|
Environment Variables:
|
|
20
21
|
LOG_LEVEL: Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
Priority:
|
|
24
|
+
1. --log-level CLI argument (highest priority)
|
|
25
|
+
2. LOG_LEVEL environment variable
|
|
26
|
+
3. --verbose flag (sets DEBUG level)
|
|
27
|
+
4. Default: WARNING (lowest priority)
|
|
22
28
|
"""
|
|
23
29
|
# Check for LOG_LEVEL environment variable
|
|
24
|
-
|
|
30
|
+
env_log_level = os.getenv("LOG_LEVEL", "").upper()
|
|
25
31
|
|
|
26
32
|
# Map string to logging level
|
|
27
33
|
level_map = {
|
|
@@ -32,13 +38,15 @@ def setup_logging(verbose: bool = False) -> None:
|
|
|
32
38
|
"CRITICAL": logging.CRITICAL,
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
# Priority: LOG_LEVEL env var > --verbose flag > default (
|
|
36
|
-
if
|
|
37
|
-
level = level_map[
|
|
41
|
+
# Priority: CLI --log-level > LOG_LEVEL env var > --verbose flag > default (WARNING)
|
|
42
|
+
if log_level:
|
|
43
|
+
level = level_map[log_level.upper()]
|
|
44
|
+
elif env_log_level in level_map:
|
|
45
|
+
level = level_map[env_log_level]
|
|
38
46
|
elif verbose:
|
|
39
47
|
level = logging.DEBUG
|
|
40
48
|
else:
|
|
41
|
-
level = logging.
|
|
49
|
+
level = logging.WARNING
|
|
42
50
|
|
|
43
51
|
logging.basicConfig(
|
|
44
52
|
level=level,
|
|
@@ -66,6 +74,14 @@ def main() -> int:
|
|
|
66
74
|
help="Show version information and exit",
|
|
67
75
|
)
|
|
68
76
|
|
|
77
|
+
# Add global log level argument
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--log-level",
|
|
80
|
+
choices=["debug", "info", "warning", "error", "critical"],
|
|
81
|
+
default=None,
|
|
82
|
+
help="Set logging level (default: warning)",
|
|
83
|
+
)
|
|
84
|
+
|
|
69
85
|
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
70
86
|
|
|
71
87
|
# Register all commands
|
|
@@ -88,8 +104,9 @@ def main() -> int:
|
|
|
88
104
|
return 1
|
|
89
105
|
|
|
90
106
|
# Setup logging
|
|
107
|
+
log_level = getattr(args, "log_level", None)
|
|
91
108
|
verbose = getattr(args, "verbose", False)
|
|
92
|
-
setup_logging(verbose)
|
|
109
|
+
setup_logging(log_level, verbose)
|
|
93
110
|
|
|
94
111
|
# Execute command
|
|
95
112
|
try:
|
|
@@ -15,21 +15,57 @@ from typing import Any
|
|
|
15
15
|
import yaml
|
|
16
16
|
|
|
17
17
|
from iam_validator.core.check_registry import CheckConfig, CheckRegistry, PolicyCheck
|
|
18
|
+
from iam_validator.core.defaults import get_default_config
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def deep_merge(base: dict, override: dict) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Deep merge two dictionaries, with override taking precedence.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
base: Base dictionary with default values
|
|
29
|
+
override: Dictionary with override values
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Merged dictionary where override values take precedence
|
|
33
|
+
"""
|
|
34
|
+
result = base.copy()
|
|
35
|
+
for key, value in override.items():
|
|
36
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
37
|
+
result[key] = deep_merge(result[key], value)
|
|
38
|
+
else:
|
|
39
|
+
result[key] = value
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
class ValidatorConfig:
|
|
23
44
|
"""Main configuration object for the validator."""
|
|
24
45
|
|
|
25
|
-
def __init__(self, config_dict: dict[str, Any] | None = None):
|
|
46
|
+
def __init__(self, config_dict: dict[str, Any] | None = None, use_defaults: bool = True):
|
|
26
47
|
"""
|
|
27
48
|
Initialize configuration from a dictionary.
|
|
28
49
|
|
|
29
50
|
Args:
|
|
30
|
-
config_dict: Dictionary loaded from YAML config file
|
|
51
|
+
config_dict: Dictionary loaded from YAML config file.
|
|
52
|
+
If None, either uses default configuration (if use_defaults=True)
|
|
53
|
+
or creates an empty configuration (if use_defaults=False).
|
|
54
|
+
If provided, merges with defaults (user config takes precedence).
|
|
55
|
+
use_defaults: Whether to load default configuration. Set to False for testing
|
|
56
|
+
or when you want an empty configuration.
|
|
31
57
|
"""
|
|
32
|
-
|
|
58
|
+
# Start with default configuration if requested
|
|
59
|
+
if use_defaults:
|
|
60
|
+
default_config = get_default_config()
|
|
61
|
+
# Merge user config with defaults if provided
|
|
62
|
+
if config_dict:
|
|
63
|
+
self.config_dict = deep_merge(default_config, config_dict)
|
|
64
|
+
else:
|
|
65
|
+
self.config_dict = default_config
|
|
66
|
+
else:
|
|
67
|
+
# No defaults - use provided config or empty dict
|
|
68
|
+
self.config_dict = config_dict or {}
|
|
33
69
|
|
|
34
70
|
# Support both nested and flat structure
|
|
35
71
|
# New flat structure: each check is a top-level key ending with "_check"
|