iam-policy-validator 1.10.2__py3-none-any.whl → 1.11.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 (27) hide show
  1. iam_policy_validator-1.11.0.dist-info/METADATA +782 -0
  2. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/RECORD +26 -22
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/action_condition_enforcement.py +27 -14
  5. iam_validator/checks/sensitive_action.py +123 -11
  6. iam_validator/checks/utils/policy_level_checks.py +47 -10
  7. iam_validator/checks/wildcard_resource.py +29 -7
  8. iam_validator/commands/__init__.py +6 -0
  9. iam_validator/commands/completion.py +420 -0
  10. iam_validator/commands/query.py +485 -0
  11. iam_validator/commands/validate.py +21 -26
  12. iam_validator/core/config/category_suggestions.py +77 -0
  13. iam_validator/core/config/condition_requirements.py +105 -54
  14. iam_validator/core/config/defaults.py +110 -6
  15. iam_validator/core/config/wildcards.py +3 -0
  16. iam_validator/core/diff_parser.py +321 -0
  17. iam_validator/core/formatters/enhanced.py +34 -27
  18. iam_validator/core/models.py +2 -0
  19. iam_validator/core/pr_commenter.py +179 -51
  20. iam_validator/core/report.py +19 -17
  21. iam_validator/integrations/github_integration.py +250 -1
  22. iam_validator/sdk/__init__.py +33 -0
  23. iam_validator/sdk/query_utils.py +454 -0
  24. iam_policy_validator-1.10.2.dist-info/METADATA +0 -549
  25. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/WHEEL +0 -0
  26. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/entry_points.txt +0 -0
  27. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -28,6 +28,13 @@ from typing import Any, Final
28
28
  IAM_PASS_ROLE_REQUIREMENT: Final[dict[str, Any]] = {
29
29
  "actions": ["iam:PassRole"],
30
30
  "severity": "high",
31
+ "suggestion_text": (
32
+ "This action allows passing IAM roles to AWS services, which can lead to privilege escalation. "
33
+ "Always restrict which services can receive roles:\n"
34
+ "• Use `iam:PassedToService` to limit specific AWS services (e.g., lambda.amazonaws.com, ecs-tasks.amazonaws.com)\n"
35
+ "• Consider adding `iam:AssociatedResourceArn` to restrict which resources can use the role\n"
36
+ "• Require MFA for sensitive role passing (`aws:MultiFactorAuthPresent` = `true`)"
37
+ ),
31
38
  "required_conditions": [
32
39
  {
33
40
  "condition_key": "iam:PassedToService",
@@ -50,66 +57,96 @@ IAM_PASS_ROLE_REQUIREMENT: Final[dict[str, Any]] = {
50
57
  ],
51
58
  }
52
59
 
53
- # S3 Write Operations - Require organization ID
54
- S3_WRITE_ORG_ID: Final[dict[str, Any]] = {
55
- "actions": ["s3:PutObject"],
60
+ # S3 Organization Boundary - Prevent data exfiltration for both reads and writes
61
+ # Enforces that S3 operations only access resources within organizational boundaries
62
+ S3_ORG_BOUNDARY: Final[dict[str, Any]] = {
63
+ "actions": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject"],
56
64
  "severity": "medium",
65
+ "suggestion_text": (
66
+ "These S3 actions can read or write data. Prevent data exfiltration by ensuring operations only access organization-owned buckets:\n"
67
+ "• Use organization ID (`aws:ResourceOrgID` = `${aws:PrincipalOrgID}`)\n"
68
+ "• OR use organization paths (`aws:ResourceOrgPaths` = `${aws:PrincipalOrgPaths}`)\n"
69
+ "• OR restrict by network boundary (IP/VPC/VPCe) + same account (`aws:ResourceAccount` = `${aws:PrincipalAccount}`)"
70
+ ),
57
71
  "required_conditions": {
58
72
  "any_of": [
59
- # Option 1: Use organization-level control with ResourceOrgID
73
+ # Option 1: Restrict to organization resources (strongest)
60
74
  {
61
- "all_of": [
62
- {
63
- "condition_key": "aws:ResourceOrgID",
64
- "description": "Restrict S3 write actions to resources within your AWS Organization",
65
- "expected_value": "${aws:PrincipalOrgID}",
66
- "example": (
67
- "{\n"
68
- ' "Condition": {\n'
69
- ' "StringEquals": {\n'
70
- ' "aws:ResourceOrgID": "${aws:PrincipalOrgID}",\n'
71
- ' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
72
- " }\n"
73
- " }\n"
74
- "}"
75
- ),
76
- },
77
- {
78
- "condition_key": "aws:ResourceAccount",
79
- "description": "Ensure the S3 resource belongs to the same AWS account as the principal",
80
- "expected_value": "${aws:PrincipalAccount}",
81
- },
82
- ]
75
+ "condition_key": "aws:ResourceOrgID",
76
+ "description": "Restrict S3 operations to resources within your AWS Organization",
77
+ "expected_value": "${aws:PrincipalOrgID}",
78
+ "example": (
79
+ "{\n"
80
+ ' "Condition": {\n'
81
+ ' "StringEquals": {\n'
82
+ ' "aws:ResourceOrgID": "${aws:PrincipalOrgID}"\n'
83
+ " }\n"
84
+ " }\n"
85
+ "}"
86
+ ),
87
+ },
88
+ # Option 2: Restrict to organization paths
89
+ {
90
+ "condition_key": "aws:ResourceOrgPaths",
91
+ "description": "Restrict S3 operations to resources within your AWS Organization path",
92
+ "expected_value": "${aws:PrincipalOrgPaths}",
93
+ "example": (
94
+ "{\n"
95
+ ' "Condition": {\n'
96
+ ' "StringEquals": {\n'
97
+ ' "aws:ResourceOrgPaths": "${aws:PrincipalOrgPaths}"\n'
98
+ " }\n"
99
+ " }\n"
100
+ "}"
101
+ ),
83
102
  },
84
- # Option 2: Use organization path-based control
103
+ # Option 3: Network boundary - Source IP + same account
85
104
  {
86
- "all_of": [
87
- {
88
- "condition_key": "aws:ResourceOrgPaths",
89
- "description": "Restrict S3 write actions to resources within your AWS Organization path",
90
- "expected_value": "${aws:PrincipalOrgPaths}",
91
- "example": (
92
- "{\n"
93
- ' "Condition": {\n'
94
- ' "StringEquals": {\n'
95
- ' "aws:ResourceOrgPaths": "${aws:PrincipalOrgPaths}",\n'
96
- ' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
97
- " }\n"
98
- " }\n"
99
- "}"
100
- ),
101
- },
102
- {
103
- "condition_key": "aws:ResourceAccount",
104
- "description": "Ensure the S3 resource belongs to the same AWS account as the principal",
105
- "expected_value": "${aws:PrincipalAccount}",
106
- },
107
- ]
105
+ "condition_key": "aws:SourceIp",
106
+ "description": "Restrict S3 operations by source IP address and same account",
107
+ "example": (
108
+ "{\n"
109
+ ' "Condition": {\n'
110
+ ' "IpAddress": {"aws:SourceIp": "10.0.0.0/8"},\n'
111
+ ' "StringEquals": {"aws:ResourceAccount": "${aws:PrincipalAccount}"}\n'
112
+ " }\n"
113
+ "}"
114
+ ),
115
+ },
116
+ # Option 4: Network boundary - Source VPC + same account
117
+ {
118
+ "condition_key": "aws:SourceVpc",
119
+ "description": "Restrict S3 operations by source VPC and same account",
120
+ "example": (
121
+ "{\n"
122
+ ' "Condition": {\n'
123
+ ' "StringEquals": {\n'
124
+ ' "aws:SourceVpc": "vpc-12345678",\n'
125
+ ' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
126
+ " }\n"
127
+ " }\n"
128
+ "}"
129
+ ),
130
+ },
131
+ # Option 5: Network boundary - VPC Endpoint + same account
132
+ {
133
+ "condition_key": "aws:SourceVpce",
134
+ "description": "Restrict S3 operations by VPC endpoint and same account",
135
+ "example": (
136
+ "{\n"
137
+ ' "Condition": {\n'
138
+ ' "StringEquals": {\n'
139
+ ' "aws:SourceVpce": "vpce-12345678",\n'
140
+ ' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
141
+ " }\n"
142
+ " }\n"
143
+ "}"
144
+ ),
108
145
  },
109
- # Option 3: Account-only control (less restrictive, but still secure)
146
+ # Option 6: Minimum - at least require same account
110
147
  {
111
148
  "condition_key": "aws:ResourceAccount",
112
- "description": "Restrict S3 write actions to resources within the same AWS account",
149
+ "description": "Restrict S3 operations to resources within the same AWS account",
113
150
  "expected_value": "${aws:PrincipalAccount}",
114
151
  "example": (
115
152
  "{\n"
@@ -130,10 +167,16 @@ SOURCE_IP_RESTRICTIONS: Final[dict[str, Any]] = {
130
167
  "action_patterns": [
131
168
  "^ssm:StartSession$",
132
169
  "^ssm:Run.*$",
133
- "^s3:GetObject$",
134
170
  "^rds-db:Connect$",
135
171
  ],
136
172
  "severity": "low",
173
+ "suggestion_text": (
174
+ "This action accesses sensitive resources or data. Restrict network access to trusted locations:\n"
175
+ "• Use `aws:SourceIp` to limit to corporate IP ranges (e.g., office networks, VPN endpoints)\n"
176
+ "• Alternative: Use `aws:SourceVpc` or `aws:SourceVpce` for VPC-based restrictions\n"
177
+ "• Consider combining with secure transport requirements\n"
178
+ "• For S3: Ensure account ownership (`aws:ResourceAccount` = `${aws:PrincipalAccount}`)"
179
+ ),
137
180
  "required_conditions": [
138
181
  {
139
182
  "condition_key": "aws:SourceIp",
@@ -146,7 +189,9 @@ SOURCE_IP_RESTRICTIONS: Final[dict[str, Any]] = {
146
189
  ' "10.0.0.0/8",\n'
147
190
  ' "172.16.0.0/12"\n'
148
191
  " ]\n"
149
- " }\n"
192
+ " },\n"
193
+ ' "Bool": {"aws:SecureTransport": "true"},\n'
194
+ ' "StringEquals": {"aws:ResourceAccount": "${aws:PrincipalAccount}"}\n'
150
195
  " }\n"
151
196
  "}"
152
197
  ),
@@ -158,6 +203,12 @@ SOURCE_IP_RESTRICTIONS: Final[dict[str, Any]] = {
158
203
  S3_SECURE_TRANSPORT: Final[dict[str, Any]] = {
159
204
  "actions": ["s3:GetObject", "s3:PutObject"],
160
205
  "severity": "critical",
206
+ "suggestion_text": (
207
+ "CRITICAL: This S3 action must enforce encrypted connections. Unencrypted HTTP connections expose data in transit:\n"
208
+ "• Set `aws:SecureTransport` to `true` to enforce HTTPS/TLS\n"
209
+ "• NEVER set `aws:SecureTransport` to `false` (this explicitly allows unencrypted connections)\n"
210
+ "• Combine with other controls (IP restrictions, account boundaries) for defense in depth"
211
+ ),
161
212
  "required_conditions": {
162
213
  "none_of": [
163
214
  {
@@ -200,7 +251,7 @@ PREVENT_PUBLIC_IP: Final[dict[str, Any]] = {
200
251
 
201
252
  CONDITION_REQUIREMENTS: Final[list[dict[str, Any]]] = [
202
253
  IAM_PASS_ROLE_REQUIREMENT,
203
- S3_WRITE_ORG_ID,
254
+ S3_ORG_BOUNDARY, # Unified S3 read/write organization boundary enforcement
204
255
  SOURCE_IP_RESTRICTIONS,
205
256
  S3_SECURE_TRANSPORT,
206
257
  PREVENT_PUBLIC_IP,
@@ -344,13 +344,41 @@ DEFAULT_CONFIG = {
344
344
  # Check for wildcard resources (Resource: "*")
345
345
  # Flags statements that apply to all resources
346
346
  # Exception: Allowed if ALL actions are in allowed_wildcards list
347
+ #
348
+ # DUAL MATCHING STRATEGY:
349
+ # The check uses two complementary matching strategies for maximum flexibility:
350
+ #
351
+ # 1. LITERAL MATCH (Fast Path - no AWS API calls):
352
+ # Policy actions match config patterns exactly as strings
353
+ # Example: Policy "iam:Get*" matches config "iam:Get*" → PASS
354
+ #
355
+ # 2. EXPANDED MATCH (Comprehensive Path - uses AWS API):
356
+ # Both policy actions and config patterns expand to actual AWS actions
357
+ # Example: Policy "iam:GetUser" matches config "iam:Get*" (expanded) → PASS
358
+ #
359
+ # SUPPORTED SCENARIOS:
360
+ # Policy Action Config Pattern Match Type Result
361
+ # iam:Get* iam:Get* Literal ✅ Pass
362
+ # iam:GetUser iam:Get* Expanded ✅ Pass
363
+ # iam:Get*, iam:List* iam:Get*, iam:List* Literal ✅ Pass
364
+ # iam:Get*, iam:GetUser iam:Get* Literal ✅ Pass
365
+ # iam:Delete* iam:Get* None ❌ Fail
366
+ #
367
+ # PERFORMANCE TIP: Literal matching is faster (no AWS API expansion)
347
368
  "wildcard_resource": {
348
369
  "enabled": True,
349
370
  "severity": "medium", # Security issue
350
371
  "description": "Checks for wildcard resources (*)",
351
372
  # Allowed wildcard patterns for actions that can be used with Resource: "*"
373
+ # Supports BOTH literal matching and pattern expansion via AWS API
374
+ #
352
375
  # Default: 25 read-only patterns (Describe*, List*, Get*)
353
376
  # See: iam_validator/core/config/wildcards.py
377
+ #
378
+ # Examples:
379
+ # ["ec2:Describe*"] # Matches: ec2:Describe* (literal) OR ec2:DescribeInstances (expanded)
380
+ # ["iam:GetUser"] # Matches: iam:GetUser only
381
+ # ["s3:List*"] # Matches: s3:List* (literal) OR s3:ListBucket (expanded)
354
382
  "allowed_wildcards": list(DEFAULT_ALLOWED_WILDCARDS),
355
383
  "message": "Statement applies to all resources (*)",
356
384
  "suggestion": "Replace wildcard with specific resource ARNs",
@@ -493,6 +521,82 @@ DEFAULT_CONFIG = {
493
521
  "ignore_patterns": [
494
522
  {"action_matches": "^iam:PassRole$"},
495
523
  ],
524
+ # Cross-statement privilege escalation patterns (policy-wide detection)
525
+ # These patterns detect dangerous action combinations across ANY statements in the policy
526
+ # Uses all_of logic: ALL actions must exist somewhere in the policy
527
+ "sensitive_actions": [
528
+ # User privilege escalation: Create user + attach admin policy
529
+ {
530
+ "all_of": ["iam:CreateUser", "iam:AttachUserPolicy"],
531
+ "severity": "critical",
532
+ "message": "Policy grants {actions} across statements - enables privilege escalation. {statements}",
533
+ "suggestion": (
534
+ "This combination allows an attacker to:\n"
535
+ "1. Create a new IAM user\n"
536
+ "2. Attach AdministratorAccess policy to that user\n"
537
+ "3. Escalate to full account access\n\n"
538
+ "Mitigation options:\n"
539
+ "• Remove both of these permissions\n"
540
+ "• Add strict IAM conditions (MFA, IP restrictions, force a specific policy with `iam:PolicyARN` condition)\n"
541
+ ),
542
+ },
543
+ # Role privilege escalation: Create role + attach admin policy
544
+ {
545
+ "all_of": ["iam:CreateRole", "iam:AttachRolePolicy"],
546
+ "severity": "high",
547
+ "message": "Policy grants {actions} across statements - enables privilege escalation. {statements}",
548
+ "suggestion": (
549
+ "This combination allows creating privileged roles with admin policies.\n\n"
550
+ "Mitigation options:\n"
551
+ "• Remove both of these permissions\n"
552
+ "• Add strict IAM conditions with a Permissions Boundary and ABAC Tagging, force a specific policy with `iam:PolicyARN` condition\n"
553
+ ),
554
+ },
555
+ # Lambda backdoor: Create/update function + invoke
556
+ {
557
+ "all_of": ["lambda:CreateFunction", "lambda:InvokeFunction"],
558
+ "severity": "medium",
559
+ "message": "Policy grants {actions} across statements - enables code execution. {statements}",
560
+ "suggestion": (
561
+ "This combination allows an attacker to:\n"
562
+ "1. Create a Lambda function with malicious code\n"
563
+ "2. Execute the function to perform operations with the Lambda's role\n\n"
564
+ "Mitigation options:\n"
565
+ "• Restrict Lambda creation to specific function names/paths\n"
566
+ "• Require resource tags on functions and tag-based invocation controls\n"
567
+ "• Require MFA for Lambda function creation\n"
568
+ "• Use separate policies for creation vs invocation"
569
+ ),
570
+ },
571
+ # Lambda code modification backdoor
572
+ {
573
+ "all_of": ["lambda:UpdateFunctionCode", "lambda:InvokeFunction"],
574
+ "severity": "medium",
575
+ "message": "Policy grants {actions} across statements - enables code injection. {statements}",
576
+ "suggestion": (
577
+ "This combination allows modifying existing Lambda functions and executing them.\n\n"
578
+ "Mitigation options:\n"
579
+ "• Use resource-based policies to restrict which functions can be modified\n"
580
+ "• Require MFA for code updates\n"
581
+ "• Use separate policies for code updates vs invocation\n"
582
+ "• Implement code signing for Lambda functions"
583
+ ),
584
+ },
585
+ # EC2 instance privilege escalation
586
+ {
587
+ "all_of": ["ec2:RunInstances", "iam:PassRole"],
588
+ "severity": "high",
589
+ "message": "Policy grants {actions} across statements - enables privilege escalation via instance profile. {statements}",
590
+ "suggestion": (
591
+ "This combination allows launching EC2 instances with privileged roles.\n\n"
592
+ "Mitigation options:\n"
593
+ "• Add iam:PassedToService condition requiring ec2.amazonaws.com\n"
594
+ "• Restrict instance creation to specific AMIs or instance types\n"
595
+ "• Limit PassRole to specific low-privilege roles\n"
596
+ "• Require tagging and ABAC controls"
597
+ ),
598
+ },
599
+ ],
496
600
  },
497
601
  # ========================================================================
498
602
  # 18. ACTION CONDITION ENFORCEMENT
@@ -505,7 +609,7 @@ DEFAULT_CONFIG = {
505
609
  # Available requirements:
506
610
  # Default (enabled):
507
611
  # - iam_pass_role: Requires iam:PassedToService
508
- # - s3_org_id: Requires organization ID for S3 writes
612
+ # - s3_org_boundary: Prevents S3 data exfiltration (reads + writes)
509
613
  # - source_ip_restrictions: Restricts to corporate IPs
510
614
  # - s3_secure_transport: Prevents insecure transport
511
615
  # - prevent_public_ip: Prevents 0.0.0.0/0 IP ranges
@@ -515,10 +619,10 @@ DEFAULT_CONFIG = {
515
619
  "enabled": True,
516
620
  "severity": "high", # Default severity (can be overridden per-requirement)
517
621
  "description": "Enforces conditions (MFA, IP, tags, etc.) for specific actions at both statement and policy level",
518
- # STATEMENT-LEVEL: Load 5 requirements from Python module
519
- # Deep copy to prevent mutation of the originals
520
- # These check individual statements independently
521
- "action_condition_requirements": __import__("copy").deepcopy(CONDITION_REQUIREMENTS),
622
+ # CRITICAL: This key is used by sensitive_action check for filtering
623
+ # It must be named "requirements" (not "action_condition_requirements")
624
+ # to enable automatic deduplication of warnings
625
+ "requirements": __import__("copy").deepcopy(CONDITION_REQUIREMENTS),
522
626
  # POLICY-LEVEL: Scan entire policy and enforce conditions across ALL matching statements
523
627
  # Example: "If ANY statement grants iam:CreateUser, then ALL such statements must have MFA"
524
628
  # Default: Empty list (opt-in feature)
@@ -543,6 +647,6 @@ def get_default_config() -> dict:
543
647
  Returns:
544
648
  A deep copy of the default configuration dictionary
545
649
  """
546
- import copy
650
+ import copy # pylint: disable=import-outside-toplevel
547
651
 
548
652
  return copy.deepcopy(DEFAULT_CONFIG)
@@ -28,8 +28,11 @@ DEFAULT_ALLOWED_WILDCARDS: Final[tuple[str, ...]] = (
28
28
  "cloudwatch:List*",
29
29
  # DynamoDB
30
30
  "dynamodb:Describe*",
31
+ "dynamodb:Get*",
32
+ "dynamodb:List*",
31
33
  # EC2
32
34
  "ec2:Describe*",
35
+ "ec2:List*",
33
36
  # Elastic Load Balancing
34
37
  "elasticloadbalancing:Describe*",
35
38
  # IAM (non-sensitive read operations)