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.

@@ -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=xkumSQPsulcb1i1qcSYPxf_hyoSt2pwqrjoqEFoAglc,206
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=3V8Wnz6BYnataKzuFMx8fHukVjzpIZaVfde9-RqZjPc,25357
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=Sr3aiLbki8_M3U9qJv7u0fM__GjJRfZzWmJVgJ3ODSw,28185
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=zdfay29HX1v4uz_LfzUUgC4-VUy8TTg5CGfZlAeEWXc,13779
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=T1SzPTu9vzk29Mey3kMfGJkE4hXTNY6ZrOsj8udDv3o,3140
24
- iam_validator/core/config_loader.py,sha256=k4D5TT_D6B9N8BbIEg0nE3wUXu0naFLHOVVJsjYZzh4,14880
25
- iam_validator/core/models.py,sha256=SUEbxDUtkX1uvgMy6-LPzomyGu82PTpXdDXZ9RKqfTY,9655
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=6k2A82EuhI72y-xCXbxRbykYcBvUP0z977pjUk9w1Cc,28977
30
- iam_validator/core/formatters/__init__.py,sha256=ggIKrI_Uu6tnKBLnUbBaXgaIXeL-JAGwUOrfSql6sow,774
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=qkKWcxETRWDr62Zmkz0NXG4oS9hj2LwDX8uH4MoSX0s,750
33
- iam_validator/core/formatters/csv.py,sha256=hyop9gddZc1eOjUvd1YJjxbo8FNlLG7Rvh6UkSCEAhU,5565
34
- iam_validator/core/formatters/html.py,sha256=kW0BVTTX8PbiEbct8mXIyqTiO6jdsGjyK6Y3NNVeRaI,19317
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=FccevVlD_mC6wtrWsya2Uo-NEphyuWsZyFar9x_ZT2g,1859
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.3.dist-info/METADATA,sha256=r2LUdyRsOAwNYbOPAXEca66sYjBYEosBBRkQG9K2YWE,31445
42
- iam_policy_validator-1.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- iam_policy_validator-1.0.3.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
44
- iam_policy_validator-1.0.3.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
45
- iam_policy_validator-1.0.3.dist-info/RECORD,,
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,,
@@ -3,5 +3,5 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.0.3"
6
+ __version__ = "1.0.4"
7
7
  __version_info__ = tuple(int(part) for part in __version__.split("."))
@@ -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 exact matches
212
- if stmt_action in actions_config:
213
- matching_actions.append(stmt_action)
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 pattern matches
216
- for pattern in action_patterns:
217
- try:
218
- if re.match(pattern, stmt_action):
219
- matching_actions.append(stmt_action)
220
- break
221
- except re.error:
222
- continue
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 = all(
235
- any(
236
- self._action_matches(stmt_action, req_action, action_patterns)
237
- for stmt_action in statement_actions
238
- )
239
- for req_action in all_of
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(stmt_action, req_action, action_patterns):
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(stmt_action, req_action, action_patterns):
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(stmt_action, forbidden_action, action_patterns):
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, statement_action: str, required_action: str, patterns: list[str]
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="Statement allows all actions (*)",
101
- suggestion="Consider limiting to specific actions needed",
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="Statement applies to all resources (*)",
119
- suggestion="Consider limiting to specific resources",
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="Statement allows all actions on all resources - CRITICAL SECURITY RISK",
135
- suggestion="This grants full administrative access. Restrict to specific actions and resources.",
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=f"Service-level wildcard '{action}' grants all permissions for {service} service",
235
+ message=message,
165
236
  action=action,
166
- suggestion=f"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*'.",
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
- message = f"Sensitive action '{matched_actions[0]}' should have conditions to limit when it can be used"
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
- message = f"Sensitive actions '{action_list}' should have conditions to limit when they can be used"
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="Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
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 == "console":
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
- verbose: Enable verbose logging
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
- Overrides the --verbose flag if set
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
- log_level_str = os.getenv("LOG_LEVEL", "").upper()
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 (INFO)
36
- if log_level_str in level_map:
37
- level = level_map[log_level_str]
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.INFO
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
- self.config_dict = config_dict or {}
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"