iam-policy-validator 1.10.0__py3-none-any.whl → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.10.0
3
+ Version: 1.10.2
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -1,6 +1,6 @@
1
1
  iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=FvUNUrZJC1gRVPHFBok-yxk6rVZGHH_JjnMPCje10kg,362
3
+ iam_validator/__version__.py,sha256=Fz08JonmD1dXoibF83ZYkSo1e0IWJO9aJ4iIAfq64mI,374
4
4
  iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=0dCH_xX-Xc0uLxtNeRjrpNjWYbdWQRzO1XNcLTSn6sI,51698
6
6
  iam_validator/checks/action_resource_matching.py,sha256=WiGJmCIJfx5yituMjZxpKmk-99N6nK20ueN02ddy9oM,19296
@@ -42,7 +42,7 @@ iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxo
42
42
  iam_validator/core/constants.py,sha256=cVBPgbXr4ALltH_NTSKsgBi6wmndLnOyUWhyBx0ZwrM,6113
43
43
  iam_validator/core/ignore_patterns.py,sha256=pZqDJBtkbck-85QK5eFPM5ZOPEKs3McRh3avqiCT5z0,10398
44
44
  iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf0TSUfc,7502
45
- iam_validator/core/models.py,sha256=f5d9ovtO1xMSwhyBrKIgc2psEq0eugnd3S3ioqurqEE,13242
45
+ iam_validator/core/models.py,sha256=yQ5iBTffdAzx88h8RyVCCmBg6kkD2zg5_lb-qLdjy3w,13386
46
46
  iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
47
47
  iam_validator/core/policy_loader.py,sha256=2KJnXzGg3g9pDXWZHk3DO0xpZnZZ-wXWFEOdQ_naJ8s,17862
48
48
  iam_validator/core/pr_commenter.py,sha256=NTKoSmjvspYX2rbl3Xn8d611XkTNSfYlGUY0zBHBP4g,16801
@@ -50,11 +50,11 @@ iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,
50
50
  iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
51
51
  iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
52
52
  iam_validator/core/aws_service/client.py,sha256=Zv7rIpEFdUCDXKGp3migPDkj8L5eZltgrGe64M2t2Ko,7336
53
- iam_validator/core/aws_service/fetcher.py,sha256=X4iI6fiLj4l9f3W6_J0E58lSP26UsBhE9gu2pzmx7Bw,22641
53
+ iam_validator/core/aws_service/fetcher.py,sha256=s_o8h6ua9gPdiV9-ElNs7YY0HlyoP0Ewtl71hTrhsZA,23340
54
54
  iam_validator/core/aws_service/parsers.py,sha256=gJzR7HCD8ItCWCCbguTQIZpPEdj2rdMwC7LPhu7ve14,5174
55
55
  iam_validator/core/aws_service/patterns.py,sha256=gGc55Tn-EJ3cmcWtmYAZROUajKYz7DaMchYWGEhHpC0,1726
56
56
  iam_validator/core/aws_service/storage.py,sha256=PrfKdvF60IL7E_8xYs_XwFoAJPRcVYw57FVLHCoqwVk,10429
57
- iam_validator/core/aws_service/validators.py,sha256=rgCScqEjXNH8xNg2R91eJbb4eIV3jZN7a6VW0n0hgA4,16347
57
+ iam_validator/core/aws_service/validators.py,sha256=L9XRJdGmR-vZ1r0bj5SCznULyKEY_G1OAjij7-kOZPM,16463
58
58
  iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
59
59
  iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
60
60
  iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
@@ -83,14 +83,14 @@ iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urch
83
83
  iam_validator/sdk/context.py,sha256=FvAEyUa_s7tHWoSdgjSkzHf1CLlYpAEmLZANxs2IJ4A,6826
84
84
  iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
85
85
  iam_validator/sdk/helpers.py,sha256=sjfK0na_Fo7O8GhEVhl44rVHqOdw6nAKkBL4FVL-QdU,5697
86
- iam_validator/sdk/policy_utils.py,sha256=CZS1OGSdiWsd2lsCwg0BDcUNWa61tUwgvn-P5rKqeN8,12987
86
+ iam_validator/sdk/policy_utils.py,sha256=bGdJ1X1aC72dVXXpAnAwyBpAiiX-qXvblpetY5BsjKU,13658
87
87
  iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
88
88
  iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
89
89
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
90
90
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
91
91
  iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
92
- iam_policy_validator-1.10.0.dist-info/METADATA,sha256=ksZBI5NOZxypD5leqm2BSUOnak43V9EeLBOw1XoLXd0,19070
93
- iam_policy_validator-1.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- iam_policy_validator-1.10.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
- iam_policy_validator-1.10.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
- iam_policy_validator-1.10.0.dist-info/RECORD,,
92
+ iam_policy_validator-1.10.2.dist-info/METADATA,sha256=t5vzMawKVACC8uFWNJznjMBvw4xX9T7z1EduP8HbAQA,19070
93
+ iam_policy_validator-1.10.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ iam_policy_validator-1.10.2.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
+ iam_policy_validator-1.10.2.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
+ iam_policy_validator-1.10.2.dist-info/RECORD,,
@@ -3,7 +3,7 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.10.0"
6
+ __version__ = "1.10.2"
7
7
  # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
- _version_base = __version__.split("-")[0] # Remove pre-release suffix if present
8
+ _version_base = __version__.split("-", maxsplit=1)[0] # Remove pre-release suffix if present
9
9
  __version_info__ = tuple(int(part) for part in _version_base.split("."))
@@ -233,8 +233,16 @@ class AWSServiceFetcher:
233
233
  await self._cache.set(services_cache_key, loaded_services)
234
234
  return loaded_services
235
235
 
236
- # Not in parsed cache, fetch the raw data from API
237
- data = await self._client.fetch(self.BASE_URL)
236
+ # Not in parsed cache, check disk cache then fetch from API
237
+ data = await self._cache.get(
238
+ f"raw:{self.BASE_URL}", url=self.BASE_URL, base_url=self.BASE_URL
239
+ )
240
+ if data is None:
241
+ data = await self._client.fetch(self.BASE_URL)
242
+ # Cache the raw data
243
+ await self._cache.set(
244
+ f"raw:{self.BASE_URL}", data, url=self.BASE_URL, base_url=self.BASE_URL
245
+ )
238
246
 
239
247
  if not isinstance(data, list):
240
248
  raise ValueError("Expected list of services from root endpoint")
@@ -247,7 +255,7 @@ class AWSServiceFetcher:
247
255
  if service and url:
248
256
  services.append(ServiceInfo(service=str(service), url=str(url)))
249
257
 
250
- # Cache the parsed services list (memory only - raw JSON already cached by client)
258
+ # Cache the parsed services list (memory only)
251
259
  await self._cache.set(services_cache_key, services)
252
260
 
253
261
  # Log only on first fetch (when parsed cache was empty)
@@ -312,13 +320,22 @@ class AWSServiceFetcher:
312
320
 
313
321
  for service in services:
314
322
  if service.service.lower() == service_name_lower:
315
- # Fetch service detail from API
316
- data = await self._client.fetch(service.url)
323
+ # Check disk cache first, then fetch from API
324
+ data = await self._cache.get(
325
+ f"raw:{service.url}", url=service.url, base_url=self.BASE_URL
326
+ )
327
+ if data is None:
328
+ # Fetch service detail from API
329
+ data = await self._client.fetch(service.url)
330
+ # Cache the raw data
331
+ await self._cache.set(
332
+ f"raw:{service.url}", data, url=service.url, base_url=self.BASE_URL
333
+ )
317
334
 
318
335
  # Validate and parse
319
336
  service_detail = ServiceDetail.model_validate(data)
320
337
 
321
- # Cache with service name as key (memory only - raw JSON already cached by client)
338
+ # Cache with service name as key (memory only)
322
339
  await self._cache.set(cache_key, service_detail)
323
340
 
324
341
  return service_detail
@@ -550,7 +567,7 @@ class AWSServiceFetcher:
550
567
  if action_pattern in ("*", "*:*"):
551
568
  return ["*"]
552
569
 
553
- service_prefix, action_name = self._parser.parse_action(action_pattern)
570
+ service_prefix, _ = self._parser.parse_action(action_pattern)
554
571
  service_detail = await self.fetch_service_by_name(service_prefix)
555
572
  available = list(service_detail.actions.keys())
556
573
  return self._parser.expand_wildcard_to_actions(action_pattern, available, service_prefix)
@@ -94,9 +94,7 @@ class ServiceValidator:
94
94
  if not allow_wildcards:
95
95
  return False, "Wildcard actions are not allowed", True
96
96
 
97
- has_matches, matched_actions = self._parser.match_wildcard_action(
98
- action_name, available_actions
99
- )
97
+ has_matches, _ = self._parser.match_wildcard_action(action_name, available_actions)
100
98
 
101
99
  if has_matches:
102
100
  # Wildcard is valid and matches at least one action
@@ -161,7 +159,7 @@ class ServiceValidator:
161
159
  get_global_conditions,
162
160
  )
163
161
 
164
- service_prefix, action_name = self._parser.parse_action(action)
162
+ _, action_name = self._parser.parse_action(action)
165
163
 
166
164
  # Check if it's a global condition key
167
165
  is_global_key = False
@@ -280,9 +278,12 @@ class ServiceValidator:
280
278
  "- `aws:RequestedRegion`\n"
281
279
  "- `aws:SourceIp`\n"
282
280
  "- `aws:SourceVpce`\n"
283
- "- `aws:UserAgent`\n"
281
+ "- `aws:ResourceOrgID`\n"
282
+ "- `aws:PrincipalOrgID`\n"
283
+ "- `aws:SourceAccount`\n"
284
+ "- `aws:PrincipalAccount`\n"
284
285
  "- `aws:CurrentTime`\n"
285
- "- `aws:SecureTransport`\n"
286
+ "- `aws:ResourceAccount`\n"
286
287
  "- `aws:PrincipalArn`\n"
287
288
  "- And many others"
288
289
  )
@@ -320,7 +321,7 @@ class ServiceValidator:
320
321
  >>> resources = validator.get_resources_for_action("s3:GetObject", service)
321
322
  """
322
323
  try:
323
- _, action_name = self._parser.parse_action(action)
324
+ _, action_name = self._parser.parse_action(action) # pylint: disable=unused-variable
324
325
 
325
326
  # Find the action (case-insensitive)
326
327
  action_detail = service_detail.actions.get(action_name)
@@ -31,7 +31,7 @@ class ServiceInfo(BaseModel):
31
31
  class ActionDetail(BaseModel):
32
32
  """Details about an AWS IAM action."""
33
33
 
34
- model_config = ConfigDict(populate_by_name=True)
34
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
35
35
 
36
36
  name: str = Field(alias="Name")
37
37
  action_condition_keys: list[str] | None = Field(
@@ -45,7 +45,7 @@ class ActionDetail(BaseModel):
45
45
  class ResourceType(BaseModel):
46
46
  """Details about an AWS resource type."""
47
47
 
48
- model_config = ConfigDict(populate_by_name=True)
48
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
49
49
 
50
50
  name: str = Field(alias="Name")
51
51
  arn_formats: list[str] | None = Field(default=None, alias="ARNFormats")
@@ -68,7 +68,7 @@ class ResourceType(BaseModel):
68
68
  class ConditionKey(BaseModel):
69
69
  """Details about an AWS condition key."""
70
70
 
71
- model_config = ConfigDict(populate_by_name=True)
71
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
72
72
 
73
73
  name: str = Field(alias="Name")
74
74
  description: str | None = Field(default=None, alias="Description")
@@ -78,7 +78,7 @@ class ConditionKey(BaseModel):
78
78
  class ServiceDetail(BaseModel):
79
79
  """Detailed information about an AWS service."""
80
80
 
81
- model_config = ConfigDict(populate_by_name=True)
81
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
82
82
 
83
83
  name: str = Field(alias="Name")
84
84
  prefix: str | None = None # Not always present in API response
@@ -106,7 +106,7 @@ class ServiceDetail(BaseModel):
106
106
  class Statement(BaseModel):
107
107
  """IAM policy statement."""
108
108
 
109
- model_config = ConfigDict(populate_by_name=True, extra="allow")
109
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
110
110
 
111
111
  sid: str | None = Field(default=None, alias="Sid")
112
112
  effect: str | None = Field(default=None, alias="Effect")
@@ -136,7 +136,7 @@ class Statement(BaseModel):
136
136
  class IAMPolicy(BaseModel):
137
137
  """IAM policy document."""
138
138
 
139
- model_config = ConfigDict(populate_by_name=True, extra="allow")
139
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
140
140
 
141
141
  version: str | None = Field(default=None, alias="Version")
142
142
  statement: list[Statement] | None = Field(default=None, alias="Statement")
@@ -63,9 +63,13 @@ def normalize_policy(policy: IAMPolicy) -> IAMPolicy:
63
63
  """
64
64
  # Pydantic model already handles this via Field(alias="Statement")
65
65
  # which expects a list, but we can ensure it's always a list
66
- statements: list[Statement] = (
67
- policy.statement if isinstance(policy.statement, list) else [policy.statement]
68
- )
66
+ if policy.statement is None:
67
+ statements: list[Statement] = []
68
+ elif isinstance(policy.statement, list):
69
+ statements = policy.statement
70
+ else:
71
+ # Single statement - wrap in list
72
+ statements = [policy.statement]
69
73
 
70
74
  # Normalize actions and resources in each statement
71
75
  normalized_statements: list[Statement] = []
@@ -118,6 +122,9 @@ def extract_actions(policy: IAMPolicy) -> list[str]:
118
122
  """
119
123
  actions = set()
120
124
 
125
+ if policy.statement is None:
126
+ return []
127
+
121
128
  for stmt in policy.statement:
122
129
  # Handle Action field
123
130
  if stmt.action:
@@ -150,6 +157,9 @@ def extract_resources(policy: IAMPolicy) -> list[str]:
150
157
  """
151
158
  resources = set()
152
159
 
160
+ if policy.statement is None:
161
+ return []
162
+
153
163
  for stmt in policy.statement:
154
164
  # Handle Resource field
155
165
  if stmt.resource:
@@ -181,12 +191,15 @@ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
181
191
  >>> keys = extract_condition_keys(policy)
182
192
  >>> print(f"Policy uses condition keys: {', '.join(keys)}")
183
193
  """
184
- condition_keys = set()
194
+ condition_keys: set[str] = set()
195
+
196
+ if policy.statement is None:
197
+ return []
185
198
 
186
199
  for stmt in policy.statement:
187
200
  if stmt.condition:
188
201
  # Condition format: {"StringEquals": {"aws:username": "johndoe"}}
189
- for operator, key_values in stmt.condition.items():
202
+ for _, key_values in stmt.condition.items():
190
203
  if isinstance(key_values, dict):
191
204
  condition_keys.update(key_values.keys())
192
205
 
@@ -212,10 +225,13 @@ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statemen
212
225
  >>> for stmt in stmts:
213
226
  ... print(f"Statement {stmt.sid} allows s3:GetObject")
214
227
  """
215
- import fnmatch
228
+ import fnmatch # pylint: disable=import-outside-toplevel
216
229
 
217
230
  matching_statements = []
218
231
 
232
+ if policy.statement is None:
233
+ return []
234
+
219
235
  for stmt in policy.statement:
220
236
  stmt_actions = stmt.get_actions()
221
237
 
@@ -246,10 +262,13 @@ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Stat
246
262
  >>> stmts = find_statements_with_resource(policy, "arn:aws:s3:::my-bucket/*")
247
263
  >>> print(f"Found {len(stmts)} statements with this resource")
248
264
  """
249
- import fnmatch
265
+ import fnmatch # pylint: disable=import-outside-toplevel
250
266
 
251
267
  matching_statements = []
252
268
 
269
+ if policy.statement is None:
270
+ return []
271
+
253
272
  for stmt in policy.statement:
254
273
  stmt_resources = stmt.get_resources()
255
274
 
@@ -286,7 +305,8 @@ def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
286
305
 
287
306
  all_statements: list[Statement] = []
288
307
  for policy in policies:
289
- all_statements.extend(policy.statement)
308
+ if policy.statement is not None:
309
+ all_statements.extend(policy.statement)
290
310
 
291
311
  # Use capitalized field names (aliases) for Pydantic model construction
292
312
  return IAMPolicy(
@@ -318,8 +338,9 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
318
338
  condition_keys = extract_condition_keys(policy)
319
339
 
320
340
  # Count allow vs deny statements
321
- allow_count = sum(1 for s in policy.statement if s.effect.lower() == "allow")
322
- deny_count = sum(1 for s in policy.statement if s.effect.lower() == "deny")
341
+ statements = policy.statement or []
342
+ allow_count = sum(1 for s in statements if s.effect and s.effect.lower() == "allow")
343
+ deny_count = sum(1 for s in statements if s.effect and s.effect.lower() == "deny")
323
344
 
324
345
  # Check for wildcards
325
346
  has_wildcard_actions = any("*" in action for action in actions)
@@ -327,7 +348,7 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
327
348
 
328
349
  return {
329
350
  "version": policy.version,
330
- "statement_count": len(policy.statement),
351
+ "statement_count": len(statements),
331
352
  "allow_statements": allow_count,
332
353
  "deny_statements": deny_count,
333
354
  "action_count": len(actions),
@@ -396,6 +417,8 @@ def is_resource_policy(policy: IAMPolicy) -> bool:
396
417
  >>> if is_resource_policy(policy):
397
418
  ... print("This is an S3 bucket policy or similar")
398
419
  """
420
+ if policy.statement is None:
421
+ return False
399
422
  return any(stmt.principal is not None for stmt in policy.statement)
400
423
 
401
424
 
@@ -414,6 +437,9 @@ def has_public_access(policy: IAMPolicy) -> bool:
414
437
  >>> if has_public_access(policy):
415
438
  ... print("WARNING: This policy allows public access!")
416
439
  """
440
+ if policy.statement is None:
441
+ return False
442
+
417
443
  for stmt in policy.statement:
418
444
  if stmt.principal == "*":
419
445
  return True