iam-policy-validator 1.14.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.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,485 @@
1
+ """Query AWS service definitions - actions, ARNs, and condition keys.
2
+
3
+ This command allows querying AWS IAM service metadata similar to policy_sentry.
4
+ Implementation inspired by: https://github.com/salesforce/policy_sentry
5
+
6
+ Examples:
7
+ # Query all actions for a service
8
+ iam-validator query action --service s3
9
+
10
+ # Query write-level actions
11
+ iam-validator query action --service s3 --access-level write
12
+
13
+ # Query actions that support wildcard resource
14
+ iam-validator query action --service s3 --resource-type "*"
15
+
16
+ # Query action details
17
+ iam-validator query action --service s3 --name GetObject
18
+
19
+ # Query ARN formats for a service
20
+ iam-validator query arn --service s3
21
+
22
+ # Query specific ARN type
23
+ iam-validator query arn --service s3 --name bucket
24
+
25
+ # Query condition keys
26
+ iam-validator query condition --service s3
27
+
28
+ # Query specific condition key
29
+ iam-validator query condition --service s3 --name s3:prefix
30
+
31
+ # Text format for simple output (great for piping)
32
+ iam-validator query action --service s3 --output text | grep Delete
33
+ iam-validator query action --service iam --access-level write --output text
34
+ """
35
+
36
+ import argparse
37
+ import json
38
+ import logging
39
+ import sys
40
+ from typing import Any
41
+
42
+ import yaml
43
+
44
+ from iam_validator.commands.base import Command
45
+ from iam_validator.core.aws_service.fetcher import AWSServiceFetcher
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class QueryCommand(Command):
51
+ """Query AWS service definitions."""
52
+
53
+ @property
54
+ def name(self) -> str:
55
+ """Command name."""
56
+ return "query"
57
+
58
+ @property
59
+ def help(self) -> str:
60
+ """Command help text."""
61
+ return "Query AWS service definitions (actions, ARNs, condition keys)"
62
+
63
+ @property
64
+ def epilog(self) -> str:
65
+ """Command epilog with examples."""
66
+ return """
67
+ examples:
68
+ # Query all actions for a service
69
+ iam-validator query action --service s3
70
+
71
+ # Query write-level actions
72
+ iam-validator query action --service s3 --access-level write
73
+
74
+ # Query actions that support wildcard resource
75
+ iam-validator query action --service s3 --resource-type "*"
76
+
77
+ # Query action details
78
+ iam-validator query action --service s3 --name GetObject
79
+
80
+ # Query ARN formats for a service
81
+ iam-validator query arn --service s3
82
+
83
+ # Query specific ARN type
84
+ iam-validator query arn --service s3 --name bucket
85
+
86
+ # Query condition keys
87
+ iam-validator query condition --service s3
88
+
89
+ # Query specific condition key
90
+ iam-validator query condition --service s3 --name s3:prefix
91
+
92
+ # Text format for simple output (great for piping)
93
+ iam-validator query action --service s3 --output text | grep Delete
94
+ iam-validator query action --service iam --access-level write --output text
95
+
96
+ note:
97
+ This feature is inspired by policy_sentry's query functionality.
98
+ See: https://github.com/salesforce/policy_sentry
99
+ """
100
+
101
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
102
+ """Add query command arguments."""
103
+ # Add subparsers for different query types
104
+ subparsers = parser.add_subparsers(
105
+ dest="query_type",
106
+ help="Type of query to perform",
107
+ required=True,
108
+ )
109
+
110
+ # Action query
111
+ action_parser = subparsers.add_parser(
112
+ "action",
113
+ help="Query IAM actions",
114
+ formatter_class=argparse.RawDescriptionHelpFormatter,
115
+ )
116
+ action_parser.add_argument(
117
+ "--service",
118
+ required=True,
119
+ help="AWS service prefix (e.g., s3, iam, ec2)",
120
+ )
121
+ action_parser.add_argument(
122
+ "--name",
123
+ help="Specific action name (e.g., GetObject, CreateUser)",
124
+ )
125
+ action_parser.add_argument(
126
+ "--access-level",
127
+ choices=["read", "write", "list", "tagging", "permissions-management"],
128
+ help="Filter by access level",
129
+ )
130
+ action_parser.add_argument(
131
+ "--resource-type",
132
+ help='Filter by resource type (use "*" for wildcard-only actions)',
133
+ )
134
+ action_parser.add_argument(
135
+ "--condition",
136
+ help="Filter actions that support specific condition key",
137
+ )
138
+ action_parser.add_argument(
139
+ "--output",
140
+ choices=["json", "yaml", "text"],
141
+ default="json",
142
+ help="Output format (default: json)",
143
+ )
144
+
145
+ # ARN query
146
+ arn_parser = subparsers.add_parser(
147
+ "arn",
148
+ help="Query ARN formats",
149
+ formatter_class=argparse.RawDescriptionHelpFormatter,
150
+ )
151
+ arn_parser.add_argument(
152
+ "--service",
153
+ required=True,
154
+ help="AWS service prefix (e.g., s3, iam, ec2)",
155
+ )
156
+ arn_parser.add_argument(
157
+ "--name",
158
+ help="Specific ARN resource type name (e.g., bucket, role)",
159
+ )
160
+ arn_parser.add_argument(
161
+ "--list-arn-types",
162
+ action="store_true",
163
+ help="List all ARN types with their formats",
164
+ )
165
+ arn_parser.add_argument(
166
+ "--output",
167
+ choices=["json", "yaml", "text"],
168
+ default="json",
169
+ help="Output format (default: json)",
170
+ )
171
+
172
+ # Condition query
173
+ condition_parser = subparsers.add_parser(
174
+ "condition",
175
+ help="Query condition keys",
176
+ formatter_class=argparse.RawDescriptionHelpFormatter,
177
+ )
178
+ condition_parser.add_argument(
179
+ "--service",
180
+ required=True,
181
+ help="AWS service prefix (e.g., s3, iam, ec2)",
182
+ )
183
+ condition_parser.add_argument(
184
+ "--name",
185
+ help="Specific condition key name (e.g., s3:prefix, iam:PolicyArn)",
186
+ )
187
+ condition_parser.add_argument(
188
+ "--output",
189
+ choices=["json", "yaml", "text"],
190
+ default="json",
191
+ help="Output format (default: json)",
192
+ )
193
+
194
+ async def execute(self, args: argparse.Namespace) -> int:
195
+ """Execute query command."""
196
+ try:
197
+ async with AWSServiceFetcher(prefetch_common=False) as fetcher:
198
+ if args.query_type == "action":
199
+ result = await self._query_action_table(fetcher, args)
200
+ elif args.query_type == "arn":
201
+ result = await self._query_arn_table(fetcher, args)
202
+ elif args.query_type == "condition":
203
+ result = await self._query_condition_table(fetcher, args)
204
+ else:
205
+ logger.error(f"Unknown query type: {args.query_type}")
206
+ return 1
207
+
208
+ # Output result
209
+ self._print_result(result, args.output)
210
+ return 0
211
+
212
+ except ValueError as e:
213
+ logger.error(f"Query failed: {e}")
214
+ return 1
215
+ except Exception as e: # pylint: disable=broad-exception-caught
216
+ logger.error(f"Unexpected error during query: {e}", exc_info=True)
217
+ return 1
218
+
219
+ def _get_access_level(self, action_detail: Any) -> str:
220
+ """Derive access level from action annotations.
221
+
222
+ AWS API provides Properties dict with boolean flags instead of AccessLevel string.
223
+ We derive the access level from these flags.
224
+ """
225
+ if not action_detail.annotations:
226
+ return "Unknown"
227
+
228
+ props = action_detail.annotations.get("Properties", {})
229
+ if not props:
230
+ return "Unknown"
231
+
232
+ # Check flags in priority order
233
+ if props.get("IsPermissionManagement"):
234
+ return "permissions-management"
235
+ if props.get("IsTaggingOnly"):
236
+ return "tagging"
237
+ if props.get("IsWrite"):
238
+ return "write"
239
+ if props.get("IsList"):
240
+ return "list"
241
+
242
+ # Default to read if none of the above
243
+ return "read"
244
+
245
+ async def _query_action_table(
246
+ self, fetcher: AWSServiceFetcher, args: argparse.Namespace
247
+ ) -> dict[str, Any] | list[dict[str, Any]]:
248
+ """Query action table."""
249
+ service_detail = await fetcher.fetch_service_by_name(args.service)
250
+
251
+ # If specific action requested, return its details
252
+ if args.name:
253
+ action_name = args.name
254
+ # Try case-insensitive lookup
255
+ action_detail = None
256
+ for key, detail in service_detail.actions.items():
257
+ if key.lower() == action_name.lower():
258
+ action_detail = detail
259
+ break
260
+
261
+ if not action_detail:
262
+ raise ValueError(f"Action '{args.name}' not found in service '{args.service}'")
263
+
264
+ access_level = self._get_access_level(action_detail)
265
+ description = (
266
+ action_detail.annotations.get("Description", "N/A")
267
+ if action_detail.annotations
268
+ else "N/A"
269
+ )
270
+
271
+ return {
272
+ "service": args.service,
273
+ "action": action_detail.name,
274
+ "description": description,
275
+ "access_level": access_level,
276
+ "resource_types": [r.get("Name", "*") for r in (action_detail.resources or [])],
277
+ "condition_keys": action_detail.action_condition_keys or [],
278
+ }
279
+
280
+ # Filter actions based on criteria
281
+ filtered_actions = []
282
+ for action_name, action_detail in service_detail.actions.items():
283
+ access_level = self._get_access_level(action_detail)
284
+
285
+ # Apply filters
286
+ if args.access_level:
287
+ if access_level.lower() != args.access_level.lower():
288
+ continue
289
+
290
+ if args.resource_type:
291
+ resources = action_detail.resources or []
292
+
293
+ # If filtering for wildcard-only actions (actions with no required resources)
294
+ if args.resource_type == "*":
295
+ # Actions with empty resources list are wildcard-only
296
+ if resources:
297
+ continue
298
+ else:
299
+ # Filter by specific resource type name
300
+ resource_names = [r.get("Name", "") for r in resources]
301
+ if args.resource_type not in resource_names:
302
+ continue
303
+
304
+ if args.condition:
305
+ condition_keys = action_detail.action_condition_keys or []
306
+ if args.condition not in condition_keys:
307
+ continue
308
+
309
+ description = (
310
+ action_detail.annotations.get("Description", "N/A")
311
+ if action_detail.annotations
312
+ else "N/A"
313
+ )
314
+
315
+ # Add to filtered list
316
+ filtered_actions.append(
317
+ {
318
+ "action": f"{args.service}:{action_name}",
319
+ "access_level": access_level,
320
+ "description": description,
321
+ }
322
+ )
323
+
324
+ return filtered_actions
325
+
326
+ async def _query_arn_table(
327
+ self, fetcher: AWSServiceFetcher, args: argparse.Namespace
328
+ ) -> dict[str, Any] | list[dict[str, Any]]:
329
+ """Query ARN table."""
330
+ service_detail = await fetcher.fetch_service_by_name(args.service)
331
+
332
+ # If specific ARN type requested
333
+ if args.name:
334
+ resource_type = None
335
+ for key, rt in service_detail.resources.items():
336
+ if key.lower() == args.name.lower():
337
+ resource_type = rt
338
+ break
339
+
340
+ if not resource_type:
341
+ raise ValueError(
342
+ f"ARN resource type '{args.name}' not found in service '{args.service}'"
343
+ )
344
+
345
+ return {
346
+ "service": args.service,
347
+ "resource_type": resource_type.name,
348
+ "arn_formats": resource_type.arn_formats or [],
349
+ "condition_keys": resource_type.condition_keys or [],
350
+ }
351
+
352
+ # List all ARN types
353
+ if args.list_arn_types:
354
+ return [
355
+ {
356
+ "resource_type": rt.name,
357
+ "arn_formats": rt.arn_formats or [],
358
+ }
359
+ for rt in service_detail.resources.values()
360
+ ]
361
+
362
+ # Return all raw ARN formats
363
+ all_arns = []
364
+ for resource_type in service_detail.resources.values():
365
+ if resource_type.arn_formats:
366
+ all_arns.extend(resource_type.arn_formats)
367
+
368
+ return list(set(all_arns)) # Remove duplicates
369
+
370
+ async def _query_condition_table(
371
+ self, fetcher: AWSServiceFetcher, args: argparse.Namespace
372
+ ) -> dict[str, Any] | list[dict[str, Any]]:
373
+ """Query condition table."""
374
+ service_detail = await fetcher.fetch_service_by_name(args.service)
375
+
376
+ # If specific condition key requested
377
+ if args.name:
378
+ condition_key = None
379
+ for key, ck in service_detail.condition_keys.items():
380
+ if key.lower() == args.name.lower():
381
+ condition_key = ck
382
+ break
383
+
384
+ if not condition_key:
385
+ raise ValueError(
386
+ f"Condition key '{args.name}' not found in service '{args.service}'"
387
+ )
388
+
389
+ return {
390
+ "service": args.service,
391
+ "condition_key": condition_key.name,
392
+ "description": condition_key.description or "N/A",
393
+ "types": condition_key.types or [],
394
+ }
395
+
396
+ # Return all condition keys
397
+ return [
398
+ {
399
+ "condition_key": ck.name,
400
+ "description": ck.description or "N/A",
401
+ "types": ck.types or [],
402
+ }
403
+ for ck in service_detail.condition_keys.values()
404
+ ]
405
+
406
+ def _print_result(self, result: Any, fmt: str) -> None:
407
+ """Print query result in specified format."""
408
+ if fmt == "yaml":
409
+ print(yaml.dump(result, default_flow_style=False, sort_keys=False))
410
+ elif fmt == "text":
411
+ self._print_text_format(result)
412
+ else: # json
413
+ print(json.dumps(result, indent=2))
414
+
415
+ def _print_text_format(self, result: Any) -> None:
416
+ """Print result in simple text format.
417
+
418
+ Text format outputs only the essential information:
419
+ - For lists of actions: one action per line (service:action format)
420
+ - For specific action: action name followed by key details
421
+ - For ARNs: one ARN format per line
422
+ - For condition keys: one condition key per line
423
+ """
424
+ if isinstance(result, list):
425
+ # List of items (actions, ARNs, or condition keys)
426
+ if not result:
427
+ return
428
+
429
+ first_item = result[0]
430
+ if "action" in first_item:
431
+ # Action list
432
+ for item in result:
433
+ print(item["action"])
434
+ elif "condition_key" in first_item:
435
+ # Condition key list
436
+ for item in result:
437
+ print(item["condition_key"])
438
+ elif "resource_type" in first_item:
439
+ # ARN type list
440
+ for item in result:
441
+ print(f"{item['resource_type']}: {', '.join(item['arn_formats'])}")
442
+ else:
443
+ # Generic list (e.g., plain ARN formats)
444
+ for item in result:
445
+ print(item)
446
+
447
+ elif isinstance(result, dict):
448
+ # Single item details
449
+ if "action" in result:
450
+ # Action details
451
+ print(result["action"])
452
+ if result.get("resource_types"):
453
+ print(f" Resource types: {', '.join(result['resource_types'])}")
454
+ if result.get("condition_keys"):
455
+ print(f" Condition keys: {', '.join(result['condition_keys'])}")
456
+ if result.get("access_level"):
457
+ print(f" Access level: {result['access_level']}")
458
+
459
+ elif "resource_type" in result:
460
+ # ARN details
461
+ print(result["resource_type"])
462
+ if result.get("arn_formats"):
463
+ for arn in result["arn_formats"]:
464
+ print(f" {arn}")
465
+ if result.get("condition_keys"):
466
+ print(f" Condition keys: {', '.join(result['condition_keys'])}")
467
+
468
+ elif "condition_key" in result:
469
+ # Condition key details
470
+ print(result["condition_key"])
471
+ if result.get("types"):
472
+ print(f" Types: {', '.join(result['types'])}")
473
+ if result.get("description") and result["description"] != "N/A":
474
+ print(f" Description: {result['description']}")
475
+
476
+
477
+ # For testing
478
+ if __name__ == "__main__":
479
+ import asyncio
480
+
481
+ cmd = QueryCommand()
482
+ arg_parser = argparse.ArgumentParser()
483
+ cmd.add_arguments(arg_parser)
484
+ parsed_args = arg_parser.parse_args()
485
+ sys.exit(asyncio.run(cmd.execute(parsed_args)))