iam-policy-validator 1.3.1__py3-none-any.whl → 1.5.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 (41) hide show
  1. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +164 -19
  2. iam_policy_validator-1.5.0.dist-info/RECORD +67 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +15 -3
  5. iam_validator/checks/action_condition_enforcement.py +1 -6
  6. iam_validator/checks/condition_key_validation.py +21 -1
  7. iam_validator/checks/full_wildcard.py +67 -0
  8. iam_validator/checks/policy_size.py +1 -0
  9. iam_validator/checks/policy_type_validation.py +299 -0
  10. iam_validator/checks/principal_validation.py +776 -0
  11. iam_validator/checks/sensitive_action.py +178 -0
  12. iam_validator/checks/service_wildcard.py +105 -0
  13. iam_validator/checks/sid_uniqueness.py +45 -7
  14. iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
  15. iam_validator/checks/wildcard_action.py +62 -0
  16. iam_validator/checks/wildcard_resource.py +131 -0
  17. iam_validator/commands/download_services.py +3 -8
  18. iam_validator/commands/post_to_pr.py +7 -0
  19. iam_validator/commands/validate.py +204 -16
  20. iam_validator/core/aws_fetcher.py +25 -12
  21. iam_validator/core/check_registry.py +25 -21
  22. iam_validator/core/config/__init__.py +83 -0
  23. iam_validator/core/config/aws_api.py +35 -0
  24. iam_validator/core/config/condition_requirements.py +535 -0
  25. iam_validator/core/config/defaults.py +390 -0
  26. iam_validator/core/config/principal_requirements.py +421 -0
  27. iam_validator/core/config/sensitive_actions.py +133 -0
  28. iam_validator/core/config/service_principals.py +95 -0
  29. iam_validator/core/config/wildcards.py +124 -0
  30. iam_validator/core/config_loader.py +29 -9
  31. iam_validator/core/formatters/enhanced.py +11 -5
  32. iam_validator/core/formatters/sarif.py +78 -14
  33. iam_validator/core/models.py +13 -3
  34. iam_validator/core/policy_checks.py +39 -6
  35. iam_validator/core/pr_commenter.py +30 -9
  36. iam_policy_validator-1.3.1.dist-info/RECORD +0 -54
  37. iam_validator/checks/security_best_practices.py +0 -535
  38. iam_validator/core/defaults.py +0 -366
  39. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
  40. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
  41. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,35 @@
1
+ """
2
+ AWS API configuration constants.
3
+
4
+ This module centralizes AWS API endpoints and related configuration
5
+ used throughout the IAM Policy Validator.
6
+ """
7
+
8
+ # AWS Service Reference API base URL
9
+ # This is the official AWS service reference that provides action, resource, and condition key metadata
10
+ AWS_SERVICE_REFERENCE_BASE_URL = "https://servicereference.us-east-1.amazonaws.com/"
11
+
12
+ # Alternative endpoints for different regions (currently not used, but available for future expansion)
13
+ AWS_SERVICE_REFERENCE_ENDPOINTS = {
14
+ "us-east-1": "https://servicereference.us-east-1.amazonaws.com/",
15
+ # Add other regional endpoints if they become available
16
+ }
17
+
18
+
19
+ def get_service_reference_url(region: str = "us-east-1") -> str:
20
+ """
21
+ Get the AWS Service Reference API URL for a specific region.
22
+
23
+ Args:
24
+ region: AWS region (default: us-east-1)
25
+
26
+ Returns:
27
+ The service reference base URL for the specified region
28
+
29
+ Example:
30
+ >>> get_service_reference_url()
31
+ 'https://servicereference.us-east-1.amazonaws.com/'
32
+ >>> get_service_reference_url("us-east-1")
33
+ 'https://servicereference.us-east-1.amazonaws.com/'
34
+ """
35
+ return AWS_SERVICE_REFERENCE_ENDPOINTS.get(region, AWS_SERVICE_REFERENCE_BASE_URL)
@@ -0,0 +1,535 @@
1
+ """
2
+ Condition requirement configurations for action_condition_enforcement check.
3
+
4
+ This module defines default condition requirements for sensitive actions,
5
+ making it easy to manage complex condition enforcement rules without
6
+ deeply nested YAML/dict structures.
7
+
8
+ Using Python provides:
9
+ - Better readability and maintainability
10
+ - Type hints and IDE support
11
+ - Easy to add/modify requirements
12
+ - No parsing overhead
13
+ - Compiled to .pyc
14
+
15
+ Configuration Fields Reference:
16
+ - description: Technical description of what the requirement does (shown in output)
17
+ - example: Concrete code example showing proper condition usage
18
+ - condition_key: The IAM condition key to validate
19
+ - expected_value: (Optional) Expected value for the condition key
20
+ - severity: (Optional) Override default severity for this requirement
21
+
22
+ Field Progression: detect (condition_key) → explain (description) → demonstrate (example)
23
+
24
+ For detailed explanation of these fields and how to customize requirements,
25
+ see: docs/condition-requirements.md and docs/configuration.md#customizing-messages
26
+ """
27
+
28
+ from typing import Any, Final
29
+
30
+ # ============================================================================
31
+ # Condition Requirement Definitions
32
+ # ============================================================================
33
+
34
+ # IAM PassRole - CRITICAL: Prevent privilege escalation
35
+ IAM_PASS_ROLE_REQUIREMENT: Final[dict[str, Any]] = {
36
+ "actions": ["iam:PassRole"],
37
+ "severity": "high",
38
+ "required_conditions": [
39
+ {
40
+ "condition_key": "iam:PassedToService",
41
+ "description": (
42
+ "Restrict which AWS services can assume the passed role to prevent privilege escalation"
43
+ ),
44
+ "example": (
45
+ '"Condition": {\n'
46
+ ' "StringEquals": {\n'
47
+ ' "iam:PassedToService": [\n'
48
+ ' "lambda.amazonaws.com",\n'
49
+ ' "ecs-tasks.amazonaws.com",\n'
50
+ ' "ec2.amazonaws.com",\n'
51
+ ' "glue.amazonaws.com"\n'
52
+ " ]\n"
53
+ " }\n"
54
+ "}"
55
+ ),
56
+ },
57
+ ],
58
+ }
59
+
60
+ # IAM Write Operations - Require permissions boundary
61
+ IAM_WRITE_PERMISSIONS_BOUNDARY: Final[dict[str, Any]] = {
62
+ "actions": [
63
+ "iam:CreateRole",
64
+ "iam:PutRolePolicy*",
65
+ "iam:PutUserPolicy",
66
+ "iam:PutRolePolicy",
67
+ "iam:Attach*Policy*",
68
+ "iam:AttachUserPolicy",
69
+ "iam:AttachRolePolicy",
70
+ ],
71
+ "severity": "high",
72
+ "required_conditions": [
73
+ {
74
+ "condition_key": "iam:PermissionsBoundary",
75
+ "description": (
76
+ "Require permissions boundary for sensitive IAM operations to prevent privilege escalation"
77
+ ),
78
+ "expected_value": "arn:aws:iam::*:policy/DeveloperBoundary",
79
+ "example": (
80
+ "# See: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html\n"
81
+ "{\n"
82
+ ' "Condition": {\n'
83
+ ' "StringEquals": {\n'
84
+ ' "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/XCompanyBoundaries"\n'
85
+ " }\n"
86
+ " }\n"
87
+ "}"
88
+ ),
89
+ },
90
+ ],
91
+ }
92
+
93
+ # S3 Write Operations - Require organization ID
94
+ S3_WRITE_ORG_ID: Final[dict[str, Any]] = {
95
+ "actions": ["s3:PutObject"],
96
+ "severity": "medium",
97
+ "required_conditions": [
98
+ {
99
+ "condition_key": "aws:ResourceOrgId",
100
+ "description": (
101
+ "Require aws:ResourceOrgId condition for S3 write actions to enforce organization-level access control"
102
+ ),
103
+ "example": (
104
+ "{\n"
105
+ ' "Condition": {\n'
106
+ ' "StringEquals": {\n'
107
+ ' "aws:ResourceOrgId": "${aws:PrincipalOrgID}"\n'
108
+ " }\n"
109
+ " }\n"
110
+ "}"
111
+ ),
112
+ },
113
+ ],
114
+ }
115
+
116
+ # IP Restrictions - Source IP requirements
117
+ SOURCE_IP_RESTRICTIONS: Final[dict[str, Any]] = {
118
+ "action_patterns": [
119
+ "^ssm:StartSession$",
120
+ "^ssm:Run.*$",
121
+ "^s3:GetObject$",
122
+ "^rds-db:Connect$",
123
+ ],
124
+ "severity": "low",
125
+ "required_conditions": [
126
+ {
127
+ "condition_key": "aws:SourceIp",
128
+ "description": "Restrict access to corporate IP ranges",
129
+ "example": (
130
+ "{\n"
131
+ ' "Condition": {\n'
132
+ ' "IpAddress": {\n'
133
+ ' "aws:SourceIp": [\n'
134
+ ' "10.0.0.0/8",\n'
135
+ ' "172.16.0.0/12"\n'
136
+ " ]\n"
137
+ " }\n"
138
+ " }\n"
139
+ "}"
140
+ ),
141
+ },
142
+ ],
143
+ }
144
+
145
+ # S3 Secure Transport - Never allow insecure transport
146
+ S3_SECURE_TRANSPORT: Final[dict[str, Any]] = {
147
+ "actions": ["s3:GetObject", "s3:PutObject"],
148
+ "required_conditions": {
149
+ "none_of": [
150
+ {
151
+ "condition_key": "aws:SecureTransport",
152
+ "expected_value": False,
153
+ "description": "Never allow insecure transport to be explicitly permitted",
154
+ "example": (
155
+ "# Set this condition to true to enforce secure transport or remove it entirely\n"
156
+ "{\n"
157
+ ' "Condition": {\n'
158
+ ' "Bool": {\n'
159
+ ' "aws:SecureTransport": "true"\n'
160
+ " }\n"
161
+ " }\n"
162
+ "}"
163
+ ),
164
+ },
165
+ ],
166
+ },
167
+ }
168
+
169
+ # ============================================================================
170
+ # Optional Requirements (Commented Examples for Users)
171
+ # ============================================================================
172
+ # These are disabled by default but can be enabled by users
173
+
174
+ # S3 Destructive Operations - Require MFA
175
+ S3_DESTRUCTIVE_MFA: Final[dict[str, Any]] = {
176
+ "actions": [
177
+ "s3:DeleteBucket",
178
+ "s3:DeleteBucketPolicy",
179
+ "s3:PutBucketPolicy",
180
+ ],
181
+ "severity": "high",
182
+ "required_conditions": [
183
+ {
184
+ "condition_key": "aws:MultiFactorAuthPresent",
185
+ "description": "Require MFA for S3 destructive operations",
186
+ "expected_value": "true",
187
+ "example": (
188
+ "{\n"
189
+ ' "Condition": {\n'
190
+ ' "Bool": {\n'
191
+ ' "aws:MultiFactorAuthPresent": "true"\n'
192
+ " }\n"
193
+ " }\n"
194
+ "}"
195
+ ),
196
+ },
197
+ ],
198
+ }
199
+
200
+ # All S3 Operations - Require HTTPS
201
+ S3_REQUIRE_HTTPS: Final[dict[str, Any]] = {
202
+ "action_patterns": ["^s3:.*"],
203
+ "severity": "medium",
204
+ "required_conditions": [
205
+ {
206
+ "condition_key": "aws:SecureTransport",
207
+ "description": "Require HTTPS for all S3 operations",
208
+ "expected_value": True,
209
+ "example": (
210
+ "{\n"
211
+ ' "Condition": {\n'
212
+ ' "Bool": {\n'
213
+ ' "aws:SecureTransport": "true"\n'
214
+ " }\n"
215
+ " }\n"
216
+ "}"
217
+ ),
218
+ },
219
+ ],
220
+ }
221
+
222
+ # EC2 Instances - Must be in specific VPCs
223
+ EC2_VPC_RESTRICTION: Final[dict[str, Any]] = {
224
+ "actions": ["ec2:RunInstances"],
225
+ "severity": "high",
226
+ "required_conditions": [
227
+ {
228
+ "condition_key": "ec2:Vpc",
229
+ "description": "EC2 instances must be launched in approved VPCs",
230
+ "example": (
231
+ "{\n"
232
+ ' "Condition": {\n'
233
+ ' "StringEquals": {\n'
234
+ ' "ec2:Vpc": "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-12345678"\n'
235
+ " }\n"
236
+ " }\n"
237
+ "}"
238
+ ),
239
+ },
240
+ ],
241
+ }
242
+
243
+ # EC2 Instances - Tag requirements (ABAC)
244
+ EC2_TAG_REQUIREMENTS: Final[dict[str, Any]] = {
245
+ "actions": ["ec2:RunInstances"],
246
+ "severity": "high",
247
+ "required_conditions": {
248
+ "all_of": [
249
+ {
250
+ "condition_key": "aws:RequestTag/env",
251
+ "operator": "StringEquals",
252
+ "expected_value": ["prod", "pre", "dev", "sandbox"],
253
+ "description": "Must specify a valid Environment tag",
254
+ },
255
+ ],
256
+ "any_of": [
257
+ {
258
+ "condition_key": "aws:ResourceTag/owner",
259
+ "operator": "StringEquals",
260
+ "expected_value": "${aws:PrincipalTag/owner}",
261
+ "description": "Resource owner must match the principal's owner tag",
262
+ },
263
+ {
264
+ "condition_key": "aws:RequestTag/owner",
265
+ "description": "Must specify resource owner",
266
+ "expected_value": "${aws:PrincipalTag/owner}",
267
+ },
268
+ ],
269
+ },
270
+ }
271
+
272
+ # RDS - Database tag requirements
273
+ RDS_TAG_REQUIREMENTS: Final[dict[str, Any]] = {
274
+ "action_patterns": [
275
+ "^rds:Create.*",
276
+ "^rds:Modify.*",
277
+ ],
278
+ "severity": "medium",
279
+ "required_conditions": {
280
+ "all_of": [
281
+ {
282
+ "condition_key": "aws:RequestTag/DataClassification",
283
+ "description": "Must specify data classification",
284
+ },
285
+ {
286
+ "condition_key": "aws:RequestTag/BackupPolicy",
287
+ "description": "Must specify backup policy",
288
+ },
289
+ {
290
+ "condition_key": "aws:RequestTag/Owner",
291
+ "description": "Must specify resource owner",
292
+ },
293
+ ],
294
+ },
295
+ }
296
+
297
+ # S3 Bucket Operations - Data classification matching
298
+ S3_BUCKET_TAG_REQUIREMENTS: Final[dict[str, Any]] = {
299
+ "actions": ["s3:CreateBucket", "s3:PutObject"],
300
+ "severity": "medium",
301
+ "required_conditions": {
302
+ "all_of": [
303
+ {
304
+ "condition_key": "aws:ResourceTag/DataClassification",
305
+ "operator": "StringEquals",
306
+ "expected_value": "${aws:PrincipalTag/DataClassification}",
307
+ "description": "Data classification must match principal's tag",
308
+ },
309
+ {
310
+ "condition_key": "aws:RequestTag/Owner",
311
+ "description": "Must specify owner",
312
+ },
313
+ {
314
+ "condition_key": "aws:RequestTag/CostCenter",
315
+ "description": "Must specify cost center",
316
+ },
317
+ ],
318
+ },
319
+ }
320
+
321
+ # Forbidden Actions - Flag if these dangerous actions appear
322
+ FORBIDDEN_ACTIONS: Final[dict[str, Any]] = {
323
+ "actions": {
324
+ "none_of": [
325
+ "iam:*",
326
+ "s3:DeleteBucket",
327
+ "s3:DeleteBucketPolicy",
328
+ ],
329
+ },
330
+ "severity": "critical",
331
+ "description": "These highly sensitive actions are forbidden in this policy",
332
+ }
333
+
334
+ # Prevent overly permissive IP ranges
335
+ PREVENT_PUBLIC_IP: Final[dict[str, Any]] = {
336
+ "action_patterns": ["^s3:.*"],
337
+ "severity": "high",
338
+ "required_conditions": {
339
+ "none_of": [
340
+ {
341
+ "condition_key": "aws:SourceIp",
342
+ "expected_value": "0.0.0.0/0",
343
+ "description": "Do not allow access from any IP address",
344
+ },
345
+ ],
346
+ },
347
+ }
348
+
349
+ # ============================================================================
350
+ # Default Requirements List
351
+ # ============================================================================
352
+
353
+ # Requirements enabled by default
354
+ DEFAULT_CONDITION_REQUIREMENTS: Final[list[dict[str, Any]]] = [
355
+ IAM_PASS_ROLE_REQUIREMENT,
356
+ IAM_WRITE_PERMISSIONS_BOUNDARY,
357
+ S3_WRITE_ORG_ID,
358
+ SOURCE_IP_RESTRICTIONS,
359
+ S3_SECURE_TRANSPORT,
360
+ ]
361
+
362
+ # All available requirements (including optional ones)
363
+ ALL_CONDITION_REQUIREMENTS: Final[dict[str, dict[str, Any]]] = {
364
+ # Default (enabled)
365
+ "iam_pass_role": IAM_PASS_ROLE_REQUIREMENT,
366
+ "iam_permissions_boundary": IAM_WRITE_PERMISSIONS_BOUNDARY,
367
+ "s3_org_id": S3_WRITE_ORG_ID,
368
+ "source_ip_restrictions": SOURCE_IP_RESTRICTIONS,
369
+ "s3_secure_transport": S3_SECURE_TRANSPORT,
370
+ # Optional (disabled by default)
371
+ "s3_destructive_mfa": S3_DESTRUCTIVE_MFA,
372
+ "s3_require_https": S3_REQUIRE_HTTPS,
373
+ "ec2_vpc_restriction": EC2_VPC_RESTRICTION,
374
+ "ec2_tag_requirements": EC2_TAG_REQUIREMENTS,
375
+ "rds_tag_requirements": RDS_TAG_REQUIREMENTS,
376
+ "s3_bucket_tag_requirements": S3_BUCKET_TAG_REQUIREMENTS,
377
+ "forbidden_actions": FORBIDDEN_ACTIONS,
378
+ "prevent_public_ip": PREVENT_PUBLIC_IP,
379
+ }
380
+
381
+
382
+ # ============================================================================
383
+ # Helper Functions
384
+ # ============================================================================
385
+
386
+
387
+ def get_default_requirements() -> list[dict[str, Any]]:
388
+ """
389
+ Get the default condition requirements.
390
+
391
+ Returns:
392
+ List of default condition requirement configurations
393
+ """
394
+ # Return a copy to prevent modification
395
+ import copy
396
+
397
+ return copy.deepcopy(DEFAULT_CONDITION_REQUIREMENTS)
398
+
399
+
400
+ def get_requirement(name: str) -> dict[str, Any] | None:
401
+ """
402
+ Get a specific requirement by name.
403
+
404
+ Args:
405
+ name: Requirement name (e.g., "iam_pass_role", "s3_destructive_mfa")
406
+
407
+ Returns:
408
+ Requirement configuration dict, or None if not found
409
+ """
410
+ import copy
411
+
412
+ req = ALL_CONDITION_REQUIREMENTS.get(name)
413
+ return copy.deepcopy(req) if req else None
414
+
415
+
416
+ def get_all_requirement_names() -> list[str]:
417
+ """
418
+ Get list of all available requirement names.
419
+
420
+ Returns:
421
+ List of requirement names
422
+ """
423
+ return list(ALL_CONDITION_REQUIREMENTS.keys())
424
+
425
+
426
+ def get_requirements_by_names(names: list[str]) -> list[dict[str, Any]]:
427
+ """
428
+ Get multiple requirements by name.
429
+
430
+ Args:
431
+ names: List of requirement names
432
+
433
+ Returns:
434
+ List of requirement configurations
435
+ """
436
+ import copy
437
+
438
+ requirements = []
439
+ for name in names:
440
+ req = ALL_CONDITION_REQUIREMENTS.get(name)
441
+ if req:
442
+ requirements.append(copy.deepcopy(req))
443
+ return requirements
444
+
445
+
446
+ def get_requirements_by_severity(
447
+ min_severity: str = "low",
448
+ ) -> list[dict[str, Any]]:
449
+ """
450
+ Get requirements filtered by minimum severity.
451
+
452
+ Args:
453
+ min_severity: Minimum severity level (low, medium, high, critical)
454
+
455
+ Returns:
456
+ List of requirements matching severity criteria
457
+ """
458
+ import copy
459
+
460
+ severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
461
+ min_level = severity_order.get(min_severity, 0)
462
+
463
+ requirements = []
464
+ for req in ALL_CONDITION_REQUIREMENTS.values():
465
+ req_severity = req.get("severity", "low")
466
+ req_level = severity_order.get(req_severity, 0)
467
+ if req_level >= min_level:
468
+ requirements.append(copy.deepcopy(req))
469
+
470
+ return requirements
471
+
472
+
473
+ def describe_requirement(name: str) -> dict[str, Any]:
474
+ """
475
+ Get description and metadata for a requirement.
476
+
477
+ Args:
478
+ name: Requirement name
479
+
480
+ Returns:
481
+ Dictionary with requirement metadata
482
+ """
483
+ descriptions = {
484
+ "iam_pass_role": {
485
+ "name": "IAM PassRole Restriction",
486
+ "category": "privilege_escalation",
487
+ "severity": "high",
488
+ "description": "Prevents privilege escalation by requiring iam:PassedToService condition",
489
+ "required": True,
490
+ },
491
+ "iam_permissions_boundary": {
492
+ "name": "IAM Permissions Boundary",
493
+ "category": "privilege_escalation",
494
+ "severity": "high",
495
+ "description": "Requires permissions boundary for IAM write operations",
496
+ "required": True,
497
+ },
498
+ "s3_org_id": {
499
+ "name": "S3 Organization ID",
500
+ "category": "data_exfiltration",
501
+ "severity": "medium",
502
+ "description": "Ensures S3 operations stay within organization",
503
+ "required": True,
504
+ },
505
+ "source_ip_restrictions": {
506
+ "name": "Source IP Restrictions",
507
+ "category": "network_security",
508
+ "severity": "low",
509
+ "description": "Restricts access to corporate IP ranges",
510
+ "required": False,
511
+ },
512
+ "s3_secure_transport": {
513
+ "name": "S3 Secure Transport",
514
+ "category": "encryption",
515
+ "severity": "medium",
516
+ "description": "Prevents explicitly allowing insecure transport",
517
+ "required": True,
518
+ },
519
+ "s3_destructive_mfa": {
520
+ "name": "S3 Destructive MFA",
521
+ "category": "data_protection",
522
+ "severity": "high",
523
+ "description": "Requires MFA for destructive S3 operations",
524
+ "required": False,
525
+ },
526
+ "ec2_tag_requirements": {
527
+ "name": "EC2 Tag Requirements (ABAC)",
528
+ "category": "abac",
529
+ "severity": "high",
530
+ "description": "Enforces tag-based access control for EC2 instances",
531
+ "required": False,
532
+ },
533
+ }
534
+
535
+ return descriptions.get(name, {"name": "Unknown", "description": "Unknown requirement"})