iam-policy-validator 1.4.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.

Files changed (56) hide show
  1. iam_policy_validator-1.4.0.dist-info/METADATA +1022 -0
  2. iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
  3. iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.4.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 +7 -0
  9. iam_validator/checks/__init__.py +27 -0
  10. iam_validator/checks/action_condition_enforcement.py +727 -0
  11. iam_validator/checks/action_resource_constraint.py +151 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +70 -0
  14. iam_validator/checks/policy_size.py +151 -0
  15. iam_validator/checks/policy_type_validation.py +299 -0
  16. iam_validator/checks/principal_validation.py +282 -0
  17. iam_validator/checks/resource_validation.py +108 -0
  18. iam_validator/checks/security_best_practices.py +536 -0
  19. iam_validator/checks/sid_uniqueness.py +170 -0
  20. iam_validator/checks/utils/__init__.py +1 -0
  21. iam_validator/checks/utils/policy_level_checks.py +143 -0
  22. iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
  23. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  24. iam_validator/commands/__init__.py +25 -0
  25. iam_validator/commands/analyze.py +434 -0
  26. iam_validator/commands/base.py +48 -0
  27. iam_validator/commands/cache.py +392 -0
  28. iam_validator/commands/download_services.py +260 -0
  29. iam_validator/commands/post_to_pr.py +86 -0
  30. iam_validator/commands/validate.py +539 -0
  31. iam_validator/core/__init__.py +14 -0
  32. iam_validator/core/access_analyzer.py +666 -0
  33. iam_validator/core/access_analyzer_report.py +643 -0
  34. iam_validator/core/aws_fetcher.py +880 -0
  35. iam_validator/core/aws_global_conditions.py +137 -0
  36. iam_validator/core/check_registry.py +469 -0
  37. iam_validator/core/cli.py +134 -0
  38. iam_validator/core/config_loader.py +452 -0
  39. iam_validator/core/defaults.py +393 -0
  40. iam_validator/core/formatters/__init__.py +27 -0
  41. iam_validator/core/formatters/base.py +147 -0
  42. iam_validator/core/formatters/console.py +59 -0
  43. iam_validator/core/formatters/csv.py +170 -0
  44. iam_validator/core/formatters/enhanced.py +434 -0
  45. iam_validator/core/formatters/html.py +672 -0
  46. iam_validator/core/formatters/json.py +33 -0
  47. iam_validator/core/formatters/markdown.py +63 -0
  48. iam_validator/core/formatters/sarif.py +187 -0
  49. iam_validator/core/models.py +298 -0
  50. iam_validator/core/policy_checks.py +656 -0
  51. iam_validator/core/policy_loader.py +396 -0
  52. iam_validator/core/pr_commenter.py +338 -0
  53. iam_validator/core/report.py +859 -0
  54. iam_validator/integrations/__init__.py +28 -0
  55. iam_validator/integrations/github_integration.py +795 -0
  56. iam_validator/integrations/ms_teams.py +442 -0
@@ -0,0 +1,434 @@
1
+ """Analyze command for IAM Policy Validator using AWS IAM Access Analyzer."""
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from iam_validator.commands.base import Command
7
+ from iam_validator.core.access_analyzer import (
8
+ AccessAnalyzerReport,
9
+ PolicyType,
10
+ ResourceType,
11
+ validate_policies_with_analyzer,
12
+ )
13
+ from iam_validator.core.access_analyzer_report import AccessAnalyzerReportFormatter
14
+ from iam_validator.core.policy_checks import validate_policies
15
+ from iam_validator.core.policy_loader import PolicyLoader
16
+ from iam_validator.core.report import ReportGenerator
17
+ from iam_validator.integrations.github_integration import GitHubIntegration
18
+
19
+
20
+ class AnalyzeCommand(Command):
21
+ """Command to analyze IAM policies using AWS IAM Access Analyzer."""
22
+
23
+ @property
24
+ def name(self) -> str:
25
+ return "analyze"
26
+
27
+ @property
28
+ def help(self) -> str:
29
+ return "Analyze IAM policies using AWS IAM Access Analyzer"
30
+
31
+ @property
32
+ def epilog(self) -> str:
33
+ return """
34
+ Examples:
35
+ # Analyze identity-based policies
36
+ iam-validator analyze --path ./policies/ --policy-type IDENTITY_POLICY
37
+
38
+ # Analyze resource-based policies (e.g., S3 bucket policies)
39
+ iam-validator analyze --path ./bucket-policies/ --policy-type RESOURCE_POLICY
40
+
41
+ # Analyze multiple paths
42
+ iam-validator analyze --path ./iam/ --path ./s3-policies/ --path bucket-policy.json
43
+
44
+ # Use specific AWS region and profile
45
+ iam-validator analyze --path ./policies/ --region us-west-2 --profile prod
46
+
47
+ # Run full validation if Access Analyzer passes
48
+ iam-validator analyze --path ./policies/ --run-all-checks
49
+
50
+ # Custom Policy Checks:
51
+
52
+ # Check that policies do NOT grant specific dangerous actions
53
+ iam-validator analyze --path ./policies/ --check-access-not-granted s3:DeleteBucket s3:DeleteObject
54
+
55
+ # Check that policies do NOT grant access to specific resources
56
+ iam-validator analyze --path ./policies/ \\
57
+ --check-access-not-granted s3:PutObject \\
58
+ --check-access-resources "arn:aws:s3:::production-bucket/*"
59
+
60
+ # Check that updated policies don't grant new access
61
+ iam-validator analyze --path ./new-policy.json \\
62
+ --check-no-new-access ./old-policy.json
63
+
64
+ # Check that S3 bucket policies don't allow public access
65
+ iam-validator analyze --path ./bucket-policy.json \\
66
+ --policy-type RESOURCE_POLICY \\
67
+ --check-no-public-access \\
68
+ --public-access-resource-type "AWS::S3::Bucket"
69
+ """
70
+
71
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
72
+ """Add analyze command arguments."""
73
+ parser.add_argument(
74
+ "--path",
75
+ "-p",
76
+ required=True,
77
+ action="append",
78
+ dest="paths",
79
+ help="Path to IAM policy file or directory (can be specified multiple times)",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "--policy-type",
84
+ "-t",
85
+ choices=["IDENTITY_POLICY", "RESOURCE_POLICY", "SERVICE_CONTROL_POLICY"],
86
+ default="IDENTITY_POLICY",
87
+ help="Type of IAM policy to validate (default: IDENTITY_POLICY)",
88
+ )
89
+
90
+ parser.add_argument(
91
+ "--region",
92
+ default="us-east-1",
93
+ help="AWS region for Access Analyzer (default: us-east-1)",
94
+ )
95
+
96
+ parser.add_argument(
97
+ "--profile",
98
+ help="AWS profile to use for Access Analyzer",
99
+ )
100
+
101
+ parser.add_argument(
102
+ "--format",
103
+ "-f",
104
+ choices=["console", "json", "markdown"],
105
+ default="console",
106
+ help="Output format (default: console)",
107
+ )
108
+
109
+ parser.add_argument(
110
+ "--output",
111
+ "-o",
112
+ help="Output file path (only for json/markdown formats)",
113
+ )
114
+
115
+ parser.add_argument(
116
+ "--no-recursive",
117
+ action="store_true",
118
+ help="Don't recursively search directories",
119
+ )
120
+
121
+ parser.add_argument(
122
+ "--fail-on-warnings",
123
+ action="store_true",
124
+ help="Fail validation if warnings are found (default: only fail on errors)",
125
+ )
126
+
127
+ parser.add_argument(
128
+ "--github-comment",
129
+ action="store_true",
130
+ help="Post validation results as GitHub PR comment",
131
+ )
132
+
133
+ parser.add_argument(
134
+ "--github-review",
135
+ action="store_true",
136
+ help="Create line-specific review comments on PR (requires --github-comment)",
137
+ )
138
+
139
+ parser.add_argument(
140
+ "--run-all-checks",
141
+ action="store_true",
142
+ help="Run full validation checks if Access Analyzer passes",
143
+ )
144
+
145
+ # Custom policy check arguments
146
+ parser.add_argument(
147
+ "--check-access-not-granted",
148
+ nargs="+",
149
+ metavar="ACTION",
150
+ help="Check that policy does NOT grant specific actions (e.g., s3:DeleteBucket)",
151
+ )
152
+
153
+ parser.add_argument(
154
+ "--check-access-resources",
155
+ nargs="+",
156
+ metavar="RESOURCE",
157
+ help="Resources to check with --check-access-not-granted (e.g., arn:aws:s3:::bucket/*)",
158
+ )
159
+
160
+ parser.add_argument(
161
+ "--check-no-new-access",
162
+ metavar="EXISTING_POLICY",
163
+ help="Path to existing policy to compare against for new access checks",
164
+ )
165
+
166
+ parser.add_argument(
167
+ "--check-no-public-access",
168
+ action="store_true",
169
+ help="Check that resource policy does not allow public access (for RESOURCE_POLICY type only)",
170
+ )
171
+
172
+ parser.add_argument(
173
+ "--public-access-resource-type",
174
+ nargs="+",
175
+ choices=[
176
+ "all", # Special value to check all types
177
+ # Storage
178
+ "AWS::S3::Bucket",
179
+ "AWS::S3::AccessPoint",
180
+ "AWS::S3::MultiRegionAccessPoint",
181
+ "AWS::S3Express::DirectoryBucket",
182
+ "AWS::S3Express::AccessPoint",
183
+ "AWS::S3::Glacier",
184
+ "AWS::S3Outposts::Bucket",
185
+ "AWS::S3Outposts::AccessPoint",
186
+ "AWS::S3Tables::TableBucket",
187
+ "AWS::S3Tables::Table",
188
+ "AWS::EFS::FileSystem",
189
+ # Database
190
+ "AWS::DynamoDB::Table",
191
+ "AWS::DynamoDB::Stream",
192
+ "AWS::OpenSearchService::Domain",
193
+ # Messaging & Streaming
194
+ "AWS::Kinesis::Stream",
195
+ "AWS::Kinesis::StreamConsumer",
196
+ "AWS::SNS::Topic",
197
+ "AWS::SQS::Queue",
198
+ # Security & Secrets
199
+ "AWS::KMS::Key",
200
+ "AWS::SecretsManager::Secret",
201
+ "AWS::IAM::AssumeRolePolicyDocument",
202
+ # Compute
203
+ "AWS::Lambda::Function",
204
+ # API & Integration
205
+ "AWS::ApiGateway::RestApi",
206
+ # DevOps & Management
207
+ "AWS::CodeArtifact::Domain",
208
+ "AWS::Backup::BackupVault",
209
+ "AWS::CloudTrail::Dashboard",
210
+ "AWS::CloudTrail::EventDataStore",
211
+ ],
212
+ default=["AWS::S3::Bucket"],
213
+ help="Resource type(s) for public access check. Use 'all' to check all 29 types. Example: all OR AWS::S3::Bucket AWS::Lambda::Function",
214
+ )
215
+
216
+ parser.add_argument(
217
+ "--verbose",
218
+ "-v",
219
+ action="store_true",
220
+ help="Enable verbose logging",
221
+ )
222
+
223
+ async def execute(self, args: argparse.Namespace) -> int:
224
+ """Execute the analyze command."""
225
+ try:
226
+ # Map string to PolicyType enum
227
+ policy_type = PolicyType[args.policy_type]
228
+
229
+ # Build custom checks configuration
230
+ custom_checks = self._build_custom_checks(args)
231
+
232
+ # Validate policies with Access Analyzer
233
+ report = validate_policies_with_analyzer(
234
+ path=args.paths,
235
+ region=args.region,
236
+ policy_type=policy_type,
237
+ profile=args.profile if hasattr(args, "profile") else None,
238
+ recursive=not args.no_recursive,
239
+ custom_checks=custom_checks,
240
+ )
241
+
242
+ # Generate report
243
+ formatter = AccessAnalyzerReportFormatter()
244
+
245
+ # Output results
246
+ if args.format == "console":
247
+ formatter.print_console_report(report)
248
+ elif args.format == "json":
249
+ if args.output:
250
+ formatter.save_json_report(report, args.output)
251
+ logging.info(f"JSON report saved to {args.output}")
252
+ else:
253
+ print(formatter.generate_json_report(report))
254
+ elif args.format == "markdown":
255
+ if args.output:
256
+ formatter.save_markdown_report(report, args.output)
257
+ logging.info(f"Markdown report saved to {args.output}")
258
+ else:
259
+ print(formatter.generate_markdown_report(report))
260
+
261
+ # Post to GitHub if configured
262
+ if args.github_comment:
263
+ async with GitHubIntegration() as github:
264
+ success = await self._post_to_github(github, report, formatter)
265
+ if not success:
266
+ logging.error("Failed to post Access Analyzer results to GitHub PR")
267
+
268
+ # Determine exit code based on validation results
269
+ if args.fail_on_warnings:
270
+ exit_code = 0 if report.total_findings == 0 else 1
271
+ else:
272
+ exit_code = 0 if report.total_errors == 0 else 1
273
+
274
+ # If Access Analyzer passes and --run-all-checks is set, run full validation
275
+ if exit_code == 0 and getattr(args, "run_all_checks", False):
276
+ exit_code = await self._run_full_validation(args)
277
+
278
+ return exit_code
279
+
280
+ except ValueError as e:
281
+ logging.error(f"Validation error: {e}")
282
+ return 1
283
+ except Exception as e:
284
+ logging.error(f"Access Analyzer validation failed: {e}", exc_info=args.verbose)
285
+ return 1
286
+
287
+ def _build_custom_checks(self, args: argparse.Namespace) -> dict | None:
288
+ """Build custom checks configuration from CLI arguments.
289
+
290
+ Args:
291
+ args: Parsed command line arguments
292
+
293
+ Returns:
294
+ Dictionary with custom check configurations, or None if no checks specified
295
+ """
296
+ custom_checks = {}
297
+
298
+ # Check access not granted
299
+ if hasattr(args, "check_access_not_granted") and args.check_access_not_granted:
300
+ custom_checks["access_not_granted"] = {
301
+ "actions": args.check_access_not_granted,
302
+ }
303
+ if hasattr(args, "check_access_resources") and args.check_access_resources:
304
+ custom_checks["access_not_granted"]["resources"] = args.check_access_resources
305
+
306
+ # Check no new access
307
+ if hasattr(args, "check_no_new_access") and args.check_no_new_access:
308
+ # Load existing policy
309
+ from iam_validator.core.policy_loader import PolicyLoader
310
+
311
+ loader = PolicyLoader()
312
+ existing_policies_loaded = loader.load_from_path(
313
+ args.check_no_new_access, recursive=False
314
+ )
315
+
316
+ if existing_policies_loaded:
317
+ # Build dict of existing policies
318
+ existing_policies_dict = {
319
+ file_path: policy.model_dump(by_alias=True, exclude_none=True)
320
+ for file_path, policy in existing_policies_loaded
321
+ }
322
+ custom_checks["no_new_access"] = {"existing_policies": existing_policies_dict}
323
+ else:
324
+ logging.warning(f"Could not load existing policy from {args.check_no_new_access}")
325
+
326
+ # Check no public access
327
+ if hasattr(args, "check_no_public_access") and args.check_no_public_access:
328
+ resource_types = getattr(args, "public_access_resource_type", ["AWS::S3::Bucket"])
329
+ # Support both single string and list
330
+ if isinstance(resource_types, str):
331
+ resource_types = [resource_types]
332
+
333
+ # Expand "all" to all resource types
334
+ if "all" in resource_types:
335
+ resource_types = [member.value for member in ResourceType]
336
+ logging.info(
337
+ f"Checking all {len(resource_types)} supported resource types for public access"
338
+ )
339
+
340
+ # Convert to ResourceType enums
341
+ resource_type_enums = [ResourceType(rt) for rt in resource_types]
342
+ custom_checks["no_public_access"] = {"resource_types": resource_type_enums}
343
+
344
+ return custom_checks if custom_checks else None
345
+
346
+ async def _run_full_validation(self, args: argparse.Namespace) -> int:
347
+ """Run full validation after Access Analyzer passes."""
348
+ logging.info("Access Analyzer validation passed. Running full validation checks...")
349
+
350
+ # Load policies again for full validation
351
+ loader = PolicyLoader()
352
+ policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
353
+
354
+ if not policies:
355
+ logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
356
+ return 1
357
+
358
+ # Run full validation
359
+ results = await validate_policies(policies)
360
+
361
+ # Generate report
362
+ generator = ReportGenerator()
363
+ validation_report = generator.generate_report(results)
364
+
365
+ # Output results
366
+ if args.format == "console":
367
+ logging.info("\n=== Full Validation Results ===")
368
+ generator.print_console_report(validation_report)
369
+ elif args.format == "json":
370
+ # Don't output JSON again if already saved
371
+ if not args.output:
372
+ print(generator.generate_json_report(validation_report))
373
+ elif args.format == "markdown":
374
+ # Don't output markdown again if already saved
375
+ if not args.output:
376
+ print(generator.generate_github_comment(validation_report))
377
+
378
+ # Post to GitHub if configured
379
+ if args.github_comment:
380
+ from iam_validator.core.pr_commenter import PRCommenter
381
+
382
+ async with GitHubIntegration() as github:
383
+ commenter = PRCommenter(github)
384
+ success = await commenter.post_findings_to_pr(
385
+ validation_report,
386
+ create_review=getattr(args, "github_review", False),
387
+ add_summary_comment=True,
388
+ )
389
+ if not success:
390
+ logging.error("Failed to post full validation to GitHub PR")
391
+
392
+ # Update exit code based on full validation
393
+ if args.fail_on_warnings:
394
+ return 0 if validation_report.total_issues == 0 else 1
395
+ else:
396
+ return 0 if validation_report.invalid_policies == 0 else 1
397
+
398
+ async def _post_to_github(
399
+ self,
400
+ github: GitHubIntegration,
401
+ report: AccessAnalyzerReport,
402
+ formatter: AccessAnalyzerReportFormatter,
403
+ ) -> bool:
404
+ """Post Access Analyzer results to GitHub PR."""
405
+ if not github.is_configured():
406
+ logging.error(
407
+ "GitHub integration not configured. "
408
+ "Set GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_PR_NUMBER"
409
+ )
410
+ return False
411
+
412
+ # Generate markdown comment (single part for now)
413
+ markdown_content = formatter.generate_markdown_report(report)
414
+
415
+ # Add identifier for updating existing comments
416
+ identifier = "<!-- iam-access-analyzer-validator -->"
417
+
418
+ # Check if content is too large for single comment
419
+ if len(markdown_content) > 60000:
420
+ # Split into multiple parts
421
+ # For simplicity, we use a basic split for Access Analyzer reports
422
+ # TODO: Implement proper multi-part splitting for Access Analyzer reports
423
+ logging.warning("Access Analyzer report is large, posting as single comment")
424
+
425
+ # Post or update comment
426
+ logging.info("Posting Access Analyzer results to PR...")
427
+ success = await github.update_or_create_comment(markdown_content, identifier)
428
+
429
+ if success:
430
+ logging.info("Successfully posted Access Analyzer results to PR")
431
+ else:
432
+ logging.error("Failed to post Access Analyzer results to PR")
433
+
434
+ return success
@@ -0,0 +1,48 @@
1
+ """Base command class for CLI commands."""
2
+
3
+ import argparse
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class Command(ABC):
8
+ """Base class for all CLI commands."""
9
+
10
+ @property
11
+ @abstractmethod
12
+ def name(self) -> str:
13
+ """Command name (e.g., 'validate', 'post-to-pr')."""
14
+ pass
15
+
16
+ @property
17
+ @abstractmethod
18
+ def help(self) -> str:
19
+ """Short help text for the command."""
20
+ pass
21
+
22
+ @property
23
+ def epilog(self) -> str | None:
24
+ """Optional epilog with examples."""
25
+ return None
26
+
27
+ @abstractmethod
28
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
29
+ """
30
+ Add command-specific arguments to the parser.
31
+
32
+ Args:
33
+ parser: ArgumentParser for this command
34
+ """
35
+ pass
36
+
37
+ @abstractmethod
38
+ async def execute(self, args: argparse.Namespace) -> int:
39
+ """
40
+ Execute the command.
41
+
42
+ Args:
43
+ args: Parsed command-line arguments
44
+
45
+ Returns:
46
+ Exit code (0 for success, non-zero for failure)
47
+ """
48
+ pass