iam-policy-validator 1.15.0__py3-none-any.whl → 1.15.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.15.0
3
+ Version: 1.15.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://boogy.github.io/iam-policy-validator
@@ -99,8 +99,16 @@ iam-validator validate --path examples/quick-start/ --format enhanced
99
99
  {
100
100
  "Version": "2012-10-17",
101
101
  "Statement": [
102
- {"Effect": "Allow", "Action": "s3:GetObjekt", "Resource": "arn:aws:s3:::my-bucket/*"},
103
- {"Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::123456789012:role/lambda-role"}
102
+ {
103
+ "Effect": "Allow",
104
+ "Action": "s3:GetObjekt",
105
+ "Resource": "arn:aws:s3:::my-bucket/*"
106
+ },
107
+ {
108
+ "Effect": "Allow",
109
+ "Action": "iam:PassRole",
110
+ "Resource": "arn:aws:iam::123456789012:role/lambda-role"
111
+ }
104
112
  ]
105
113
  }
106
114
  ```
@@ -111,7 +119,11 @@ iam-validator validate --path examples/quick-start/ --format enhanced
111
119
  {
112
120
  "Version": "2012-10-17",
113
121
  "Statement": [
114
- {"Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/*"}
122
+ {
123
+ "Effect": "Allow",
124
+ "Action": "s3:GetObject",
125
+ "Resource": "arn:aws:s3:::my-bucket/*"
126
+ }
115
127
  ]
116
128
  }
117
129
  ```
@@ -122,7 +134,11 @@ iam-validator validate --path examples/quick-start/ --format enhanced
122
134
  {
123
135
  "Version": "2012-10-17",
124
136
  "Statement": [
125
- {"Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function"}
137
+ {
138
+ "Effect": "Allow",
139
+ "Action": "lambda:InvokeFunction",
140
+ "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function"
141
+ }
126
142
  ]
127
143
  }
128
144
  ```
@@ -228,7 +244,8 @@ action_condition_enforcement:
228
244
  description: "Restrict which services can use passed roles"
229
245
 
230
246
  # Enforce IP restrictions for privileged actions (automation from CI/CD)
231
- - actions: ["iam:AttachUserPolicy", "iam:PutUserPolicy", "iam:CreateAccessKey"]
247
+ - actions:
248
+ ["iam:AttachUserPolicy", "iam:PutUserPolicy", "iam:CreateAccessKey"]
232
249
  required_conditions:
233
250
  - condition_key: "aws:SourceIp"
234
251
  expected_value: ["10.0.0.0/8", "172.16.0.0/12"]
@@ -270,9 +287,17 @@ Privilege escalation often occurs when multiple actions are scattered across dif
270
287
  ```json
271
288
  {
272
289
  "Statement": [
273
- {"Sid": "AllowUserManagement", "Action": "iam:CreateUser", "Resource": "*"},
274
- {"Sid": "AllowS3Read", "Action": "s3:GetObject", "Resource": "*"},
275
- {"Sid": "AllowPolicyAttachment", "Action": "iam:AttachUserPolicy", "Resource": "*"}
290
+ {
291
+ "Sid": "AllowUserManagement",
292
+ "Action": "iam:CreateUser",
293
+ "Resource": "*"
294
+ },
295
+ { "Sid": "AllowS3Read", "Action": "s3:GetObject", "Resource": "*" },
296
+ {
297
+ "Sid": "AllowPolicyAttachment",
298
+ "Action": "iam:AttachUserPolicy",
299
+ "Resource": "*"
300
+ }
276
301
  ]
277
302
  }
278
303
  ```
@@ -408,9 +433,9 @@ iam-validator validate --path policies/ --aws-services-dir ./aws-services
408
433
  - uses: boogy/iam-policy-validator@v1
409
434
  with:
410
435
  path: policies/
411
- github-review: true # Inline PR comments
412
- github-summary: true # Actions summary tab
413
- fail-on-severity: high # Block merge on high/critical
436
+ github-review: true # Inline PR comments
437
+ github-summary: true # Actions summary tab
438
+ fail-on-severity: high # Block merge on high/critical
414
439
  ```
415
440
 
416
441
  ---
@@ -440,14 +465,14 @@ Validates against official AWS IAM requirements:
440
465
 
441
466
  Identifies overly permissive configurations:
442
467
 
443
- | Check | What It Catches |
444
- | ------------------------- | ------------------------------------------------------ |
445
- | **Wildcard Action** | `Action: "*"` grants all AWS permissions |
446
- | **Wildcard Resource** | `Resource: "*"` applies to all resources |
447
- | **Full Wildcard** | Both `Action: "*"` AND `Resource: "*"` (admin access) |
448
- | **Service Wildcards** | `s3:*`, `iam:*`, `ec2:*` (overly broad) |
468
+ | Check | What It Catches |
469
+ | ------------------------- | -------------------------------------------------------- |
470
+ | **Wildcard Action** | `Action: "*"` grants all AWS permissions |
471
+ | **Wildcard Resource** | `Resource: "*"` applies to all resources |
472
+ | **Full Wildcard** | Both `Action: "*"` AND `Resource: "*"` (admin access) |
473
+ | **Service Wildcards** | `s3:*`, `iam:*`, `ec2:*` (overly broad) |
449
474
  | **Sensitive Actions** | 490+ privilege escalation patterns and dangerous actions |
450
- | **Condition Enforcement** | Organization-specific condition requirements |
475
+ | **Condition Enforcement** | Organization-specific condition requirements |
451
476
 
452
477
  **Note on Sensitive Actions:** This check has two modes:
453
478
 
@@ -540,7 +565,7 @@ action_condition_enforcement:
540
565
  - actions: ["iam:CreateUser", "iam:DeleteUser", "iam:CreateAccessKey"]
541
566
  required_conditions:
542
567
  - condition_key: "aws:SourceIp"
543
- expected_value: ["10.0.0.0/8", "52.94.76.0/24"] # Corporate + GitHub Actions
568
+ expected_value: ["10.0.0.0/8", "52.94.76.0/24"] # Corporate + GitHub Actions
544
569
 
545
570
  # Ignore patterns
546
571
  ignore_patterns:
@@ -677,19 +702,19 @@ iam-validator analyze --path new-policy.json \
677
702
 
678
703
  ## Comparison Matrix
679
704
 
680
- | Feature | IAM Policy Validator | IAM Lens | IAMSpy | Policy Sentry |
681
- | ------------------------------ | -------------------------------- | ----------------------------- | ---------------------- | -------------------------- |
682
- | **Primary Purpose** | Pre-deployment validation | Runtime permission analysis | Permission enumeration | Least-privilege generation |
683
- | **Use Case** | CI/CD policy scanning | "What can this principal do?" | Pentesting/audit | Policy creation |
684
- | **Custom Security Rules** | ✅ Full support | ❌ No | ❌ No | ❌ No |
705
+ | Feature | IAM Policy Validator | IAM Lens | IAMSpy | Policy Sentry |
706
+ | ------------------------------ | --------------------------------- | ----------------------------- | ---------------------- | -------------------------- |
707
+ | **Primary Purpose** | Pre-deployment validation | Runtime permission analysis | Permission enumeration | Least-privilege generation |
708
+ | **Use Case** | CI/CD policy scanning | "What can this principal do?" | Pentesting/audit | Policy creation |
709
+ | **Custom Security Rules** | ✅ Full support | ❌ No | ❌ No | ❌ No |
685
710
  | **Cross-Statement Patterns** | ✅ Privilege escalation detection | N/A (different purpose) | N/A | N/A |
686
- | **Action-Resource Validation** | ✅ Catches incompatible pairs | N/A | ❌ No | ✅ Generates correct |
687
- | **Organization Conditions** | ✅ IP, tags, encryption, etc. | ❌ No | ❌ No | ❌ No |
688
- | **CI/CD Ready** | ✅ GitHub Actions native | ⚠️ Manual setup | ⚠️ Manual | ⚠️ Manual |
689
- | **PR Line Comments** | ✅ Diff-aware | ❌ No | ❌ No | ❌ No |
690
- | **AWS Service Data** | ✅ Official API (auto-update) | ✅ Real AWS account data | ⚠️ Static | ✅ Official API |
691
- | **Offline Mode** | ✅ Yes | ❌ Needs AWS account | ✅ Yes | ❌ Needs internet |
692
- | **Query Permissions** | ✅ Yes | ✅ Yes (different approach) | ⚠️ Enumerate only | ✅ Excellent |
711
+ | **Action-Resource Validation** | ✅ Catches incompatible pairs | N/A | ❌ No | ✅ Generates correct |
712
+ | **Organization Conditions** | ✅ IP, tags, encryption, etc. | ❌ No | ❌ No | ❌ No |
713
+ | **CI/CD Ready** | ✅ GitHub Actions native | ⚠️ Manual setup | ⚠️ Manual | ⚠️ Manual |
714
+ | **PR Line Comments** | ✅ Diff-aware | ❌ No | ❌ No | ❌ No |
715
+ | **AWS Service Data** | ✅ Official API (auto-update) | ✅ Real AWS account data | ⚠️ Static | ✅ Official API |
716
+ | **Offline Mode** | ✅ Yes | ❌ Needs AWS account | ✅ Yes | ❌ Needs internet |
717
+ | **Query Permissions** | ✅ Yes | ✅ Yes (different approach) | ⚠️ Enumerate only | ✅ Excellent |
693
718
 
694
719
  **Choose this tool if you:**
695
720
 
@@ -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=nBQw8c9_j3cm0AjlGzltWfPi7BYKIJlC0VQImhHNMGQ,374
3
+ iam_validator/__version__.py,sha256=Bgc0qLeAX9DtaxWp2EJRUbZweOqF_-YeAxjCOs2BIuM,374
4
4
  iam_validator/checks/__init__.py,sha256=wFU5Lz-ZIQBcn2y1u0Kl88B--vEO3btOOaTGPPSjJ74,2106
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=2-XUMbof9tQ7SHZNmAHMkR1DgbOIzY2eFWlp9S9dwLk,60625
6
6
  iam_validator/checks/action_resource_matching.py,sha256=qND0hfDgNoxFEdLWwrxOPVDfdj3k50nzedT2qF7nK7o,19428
@@ -13,7 +13,7 @@ iam_validator/checks/not_action_not_resource.py,sha256=WWKOCLCq7yxOG9tgi1n5xPpph
13
13
  iam_validator/checks/policy_size.py,sha256=eJd36Nj4gqWLIkQ5imhHR1hGtQ6T-iJsC22Wd1VSUf0,4681
14
14
  iam_validator/checks/policy_structure.py,sha256=LExdm93JeqsyhikWrh9lfJ9sBiZ4818Ts0-N3MwUrtk,22089
15
15
  iam_validator/checks/policy_type_validation.py,sha256=-Q2Yn_sa7Cba_Fb9ESxSL5qNbRpncuC7NQy7R_fdJtY,16521
16
- iam_validator/checks/principal_validation.py,sha256=TthgDW5YXFfv5li1u4nn56k0Feqt5HeOEtQgo5pBKqo,27911
16
+ iam_validator/checks/principal_validation.py,sha256=oXSooVQIgUfbTRulgpiLACSJ2LZXM0Q4I00tOX3YTAI,34679
17
17
  iam_validator/checks/resource_validation.py,sha256=k7qHIwX7IDf4MCWIvl9G17aINzTuZLOHDHRWrujbCaM,7787
18
18
  iam_validator/checks/sensitive_action.py,sha256=LHicl6dd5E1JV19cLKvFAMGzLzYr5h_Y7QvS8s57kvA,18952
19
19
  iam_validator/checks/service_wildcard.py,sha256=1epcET5oDclAmzxhtmQfKBg3Q5Rl4VXBMoZouxCJLpM,4114
@@ -44,7 +44,7 @@ iam_validator/core/aws_fetcher.py,sha256=op93QvtGmeLF9dHobl2IuoPDeunn33pBLb8h7Xj
44
44
  iam_validator/core/check_registry.py,sha256=cRvFko_cTrip94VgVqwkxgrjR6oz3JfSiwertESicRc,28567
45
45
  iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
46
46
  iam_validator/core/codeowners.py,sha256=dfRjYTpcTVmc-h95i4EoPXCXlcblD8yryeJBaTKQfjM,7530
47
- iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
47
+ iam_validator/core/condition_validators.py,sha256=5IdCZG0w8qQL37_wYv8TPQD3rIsmrB-845Ff4N-WXDU,27357
48
48
  iam_validator/core/constants.py,sha256=O80dQ6AtgsCJPempRtNlKaSIVE61rg6YDPV5vgaSnAY,7771
49
49
  iam_validator/core/diff_parser.py,sha256=5Jxa6WvQZtG5grblZeUH2OQ2R46tFLK-h8tvkHOSfLk,12110
50
50
  iam_validator/core/finding_fingerprint.py,sha256=NJIlu8NhdenWbLS7ww8LyWFasJgpKWN63-DprrNW7Zs,4353
@@ -64,15 +64,15 @@ iam_validator/core/aws_service/fetcher.py,sha256=TqaCp6yKOEXCJdcQIFVBB5RW0pxLMOm
64
64
  iam_validator/core/aws_service/parsers.py,sha256=gJzR7HCD8ItCWCCbguTQIZpPEdj2rdMwC7LPhu7ve14,5174
65
65
  iam_validator/core/aws_service/patterns.py,sha256=gGc55Tn-EJ3cmcWtmYAZROUajKYz7DaMchYWGEhHpC0,1726
66
66
  iam_validator/core/aws_service/storage.py,sha256=A98ui_THAyZ86ik-t6HWB22vOqwmbFklG10uhdf26p4,10881
67
- iam_validator/core/aws_service/validators.py,sha256=qWMB4iQi9Oc0SGXOJVrlyjnsMwmPlqhUaywyRL4d-hM,19385
67
+ iam_validator/core/aws_service/validators.py,sha256=d2nGuy4NifbBiKbLIFHxP-wZctYDtK4qniSdq3l8-T0,21574
68
68
  iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
69
69
  iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
70
- iam_validator/core/config/aws_global_conditions.py,sha256=Ny20rnwp0OGxW8NdrHw8d3Lv3wh8YbL5vCIFQw7ZQh4,8006
70
+ iam_validator/core/config/aws_global_conditions.py,sha256=WQjR8z1Mhc8H874CnzwEMvKOz9rCifBNUltvo0oFYbM,7894
71
71
  iam_validator/core/config/category_suggestions.py,sha256=fopaZ9kXDrsLgi_r0pERrLwgdPPJl5VIiKvXtQK9tj0,8583
72
72
  iam_validator/core/config/check_documentation.py,sha256=-wK6-wfU7SEHX3h2Jj5pOFqgxD9ULW-NvEkSVp_66WA,18718
73
73
  iam_validator/core/config/condition_requirements.py,sha256=1CeQJfWV-Y2ImW0Mq9YdrgvH-hj9IXe0gVOm3B36Rc8,10655
74
74
  iam_validator/core/config/config_loader.py,sha256=Fohz9R-H5edYMYXoT3HkjmsmZE32rjLWlAjnZaz1Xrc,25649
75
- iam_validator/core/config/defaults.py,sha256=EKlmfX04IGPb4Qs9ctqrStoJ-_LAGEr2PWTJX9fjenk,38920
75
+ iam_validator/core/config/defaults.py,sha256=z5gSBi3r5h2iBmjH7lKuLaYQY9oSgSBqXgWxxIr33gw,39956
76
76
  iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
77
77
  iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
78
78
  iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
@@ -112,8 +112,8 @@ iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYu
112
112
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
113
113
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
114
114
  iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
115
- iam_policy_validator-1.15.0.dist-info/METADATA,sha256=CkJmAGo-FcnQ0qpEb0_EDjNcbKRzTErYiIic3gUyHD8,34808
116
- iam_policy_validator-1.15.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
117
- iam_policy_validator-1.15.0.dist-info/entry_points.txt,sha256=VXAcx1evo9fuxX0Gtj3J2HnzWcBHSXugiZwBtQ1BXE0,162
118
- iam_policy_validator-1.15.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
119
- iam_policy_validator-1.15.0.dist-info/RECORD,,
115
+ iam_policy_validator-1.15.2.dist-info/METADATA,sha256=3YWLi7WFFLI4s-vCyjfkxJ8WB9QJCQthXl1-_isP9wE,34939
116
+ iam_policy_validator-1.15.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
117
+ iam_policy_validator-1.15.2.dist-info/entry_points.txt,sha256=VXAcx1evo9fuxX0Gtj3J2HnzWcBHSXugiZwBtQ1BXE0,162
118
+ iam_policy_validator-1.15.2.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
119
+ iam_policy_validator-1.15.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.15.0"
6
+ __version__ = "1.15.2"
7
7
  # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
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("."))
@@ -3,6 +3,7 @@
3
3
  Validates Principal elements in resource-based policies for security best practices.
4
4
  This check enforces:
5
5
  - Blocked principals (e.g., public access via "*")
6
+ - Service principal wildcards (e.g., {"Service": "*"} - extremely dangerous)
6
7
  - Allowed principals whitelist (optional)
7
8
  - Rich condition requirements for principals (supports any_of/all_of/none_of)
8
9
  - Service principal validation
@@ -77,21 +78,55 @@ class PrincipalValidationCheck(PolicyCheck):
77
78
  return issues
78
79
 
79
80
  # Get configuration (defaults match defaults.py)
80
- blocked_principals = config.config.get("blocked_principals", ["*"])
81
+ blocked_principals = list(config.config.get("blocked_principals", []))
81
82
  allowed_principals = config.config.get("allowed_principals", [])
82
83
  principal_condition_requirements = config.config.get("principal_condition_requirements", [])
83
84
  # Default: "aws:*" allows ALL AWS service principals (*.amazonaws.com)
84
- # This matches the default in defaults.py:251
85
+ # This matches the default in defaults.py
85
86
  allowed_service_principals = config.config.get("allowed_service_principals", ["aws:*"])
87
+ # Default: block service principal wildcards ({"Service": "*"})
88
+ # This is extremely dangerous as it allows ANY AWS service to assume the role
89
+ block_service_principal_wildcard = config.config.get(
90
+ "block_service_principal_wildcard", True
91
+ )
92
+ # block_wildcard_principal: Strict mode for Principal: "*"
93
+ # false (default): Allow wildcard principal but require conditions
94
+ # true: Block wildcard principal entirely, skip condition checks
95
+ block_wildcard_principal = config.config.get("block_wildcard_principal", False)
96
+ if block_wildcard_principal and "*" not in blocked_principals:
97
+ blocked_principals.append("*")
98
+
99
+ # Check for service principal wildcards FIRST (highest priority security issue)
100
+ # If detected, return early - no conditions can make {"Service": "*"} safe
101
+ if block_service_principal_wildcard:
102
+ service_wildcard_issues = self._check_service_principal_wildcards(
103
+ statement, statement_idx, config
104
+ )
105
+ if service_wildcard_issues:
106
+ # Return early - this is unfixable, don't suggest conditions
107
+ return service_wildcard_issues
86
108
 
87
109
  # Extract principals from statement
88
110
  principals = self._extract_principals(statement)
89
111
 
112
+ # Track blocked principals to skip condition checks for them
113
+ blocked_principal_values: set[str] = set()
114
+
115
+ # Check if statement has {"Service": "*"} pattern
116
+ # If so, we shouldn't also flag the * as a blocked principal
117
+ has_service_wildcard = self._has_service_principal_wildcard(statement)
118
+
90
119
  for principal in principals:
120
+ # Skip blocking check for "*" if it came from {"Service": "*"}
121
+ # That case is handled by _check_service_principal_wildcards
122
+ if principal == "*" and has_service_wildcard:
123
+ continue
124
+
91
125
  # Check if principal is blocked
92
126
  if self._is_blocked_principal(
93
127
  principal, blocked_principals, allowed_service_principals
94
128
  ):
129
+ blocked_principal_values.add(principal)
95
130
  issues.append(
96
131
  ValidationIssue(
97
132
  severity=self.get_severity(config),
@@ -129,15 +164,19 @@ class PrincipalValidationCheck(PolicyCheck):
129
164
  continue
130
165
 
131
166
  # Check principal_condition_requirements (supports any_of/all_of/none_of)
167
+ # Skip condition checks for principals that are already blocked
132
168
  if principal_condition_requirements:
133
- condition_issues = self._validate_principal_condition_requirements(
134
- statement,
135
- statement_idx,
136
- principals,
137
- principal_condition_requirements,
138
- config,
139
- )
140
- issues.extend(condition_issues)
169
+ # Filter out blocked principals - they need to be removed, not conditioned
170
+ principals_to_check = [p for p in principals if p not in blocked_principal_values]
171
+ if principals_to_check:
172
+ condition_issues = self._validate_principal_condition_requirements(
173
+ statement,
174
+ statement_idx,
175
+ principals_to_check,
176
+ principal_condition_requirements,
177
+ config,
178
+ )
179
+ issues.extend(condition_issues)
141
180
 
142
181
  return issues
143
182
 
@@ -178,6 +217,106 @@ class PrincipalValidationCheck(PolicyCheck):
178
217
 
179
218
  return principals
180
219
 
220
+ def _has_service_principal_wildcard(self, statement: Statement) -> bool:
221
+ """Check if statement has {"Service": "*"} pattern.
222
+
223
+ This is used to avoid double-flagging - if the statement has a service
224
+ principal wildcard, we shouldn't also block it as a regular wildcard.
225
+ """
226
+ if statement.principal and isinstance(statement.principal, dict):
227
+ service_principals = statement.principal.get("Service")
228
+ if service_principals:
229
+ if isinstance(service_principals, str) and service_principals == "*":
230
+ return True
231
+ if isinstance(service_principals, list) and "*" in service_principals:
232
+ return True
233
+ return False
234
+
235
+ def _check_service_principal_wildcards(
236
+ self,
237
+ statement: Statement,
238
+ statement_idx: int,
239
+ _config: CheckConfig,
240
+ ) -> list[ValidationIssue]:
241
+ """Check for dangerous service principal wildcards in Principal field.
242
+
243
+ Detects patterns like:
244
+ - "Principal": {"Service": "*"}
245
+ - "Principal": {"Service": ["*"]}
246
+ - "Principal": {"Service": ["lambda.amazonaws.com", "*"]}
247
+
248
+ These are dangerous because they allow ANY AWS service to access the resource
249
+ or assume the role. Without proper source verification conditions (aws:SourceArn,
250
+ aws:SourceAccount), any service in any account could potentially access the resource.
251
+
252
+ Note: NotPrincipal with {"Service": "*"} is NOT flagged here because it means
253
+ "allow everyone EXCEPT all services" - a different concern (overly broad exclusion)
254
+ but not an overly permissive grant.
255
+
256
+ Args:
257
+ statement: The statement to check
258
+ statement_idx: Index of the statement
259
+ config: Check configuration
260
+
261
+ Returns:
262
+ List of validation issues
263
+ """
264
+ issues: list[ValidationIssue] = []
265
+
266
+ # Only check Principal field (not NotPrincipal)
267
+ # NotPrincipal: {"Service": "*"} means "everyone EXCEPT services" which is
268
+ # a different concern (overly broad exclusion) but not an overly permissive grant
269
+ if statement.principal is None:
270
+ return issues
271
+
272
+ if not isinstance(statement.principal, dict):
273
+ return issues
274
+
275
+ # Check the "Service" key specifically
276
+ service_principals = statement.principal.get("Service")
277
+ if service_principals is None:
278
+ return issues
279
+
280
+ # Normalize to list
281
+ if isinstance(service_principals, str):
282
+ service_principals = [service_principals]
283
+
284
+ # Check for wildcard in service principals
285
+ for service_principal in service_principals:
286
+ if service_principal == "*":
287
+ issues.append(
288
+ ValidationIssue(
289
+ severity="critical", # Always critical - extremely permissive
290
+ issue_type="service_principal_wildcard",
291
+ message=(
292
+ 'Dangerous service principal wildcard: `"Principal": {"Service": "*"}`. '
293
+ "This allows ANY AWS service to access this resource or assume this role. "
294
+ "Without source verification conditions, this creates an overly permissive "
295
+ "trust relationship."
296
+ ),
297
+ statement_index=statement_idx,
298
+ statement_sid=statement.sid,
299
+ line_number=statement.line_number,
300
+ suggestion=(
301
+ "Replace the wildcard with specific AWS service principals and add "
302
+ "source verification conditions:\n\n"
303
+ "1. Specify exact services:\n"
304
+ ' `"Principal": {"Service": "lambda.amazonaws.com"}`\n\n'
305
+ "2. Add source conditions:\n"
306
+ "```json\n"
307
+ ' "Condition": {\n'
308
+ ' "ArnLike": {"aws:SourceArn": "arn:aws:lambda:*:ACCOUNT:function:*"},\n'
309
+ ' "StringEquals": {"aws:SourceAccount": "ACCOUNT"}\n\n'
310
+ " }\n"
311
+ "```\n"
312
+ ),
313
+ field_name="principal",
314
+ )
315
+ )
316
+ break # One issue is enough
317
+
318
+ return issues
319
+
181
320
  def _is_blocked_principal(
182
321
  self, principal: str, blocked_list: list[str], service_whitelist: list[str]
183
322
  ) -> bool:
@@ -243,22 +243,30 @@ class ServiceValidator:
243
243
  _, action_name = self._parser.parse_action(action)
244
244
 
245
245
  # Check if it's a global condition key
246
+ # Note: Some aws: prefixed keys like aws:RequestTag/* and aws:ResourceTag/* are NOT
247
+ # global keys - they're action-specific or resource-specific. We'll check those later.
246
248
  is_global_key = False
247
249
  if condition_key.startswith("aws:"):
248
250
  global_conditions = get_global_conditions()
249
251
  if global_conditions.is_valid_global_key(condition_key):
250
252
  is_global_key = True
251
- else:
252
- return ConditionKeyValidationResult(
253
- is_valid=False,
254
- error_message=f"Invalid AWS global condition key: `{condition_key}`.",
255
- )
253
+ # If not a global key, continue to check action/resource-specific keys
254
+ # Don't return an error yet - aws:RequestTag, aws:ResourceTag are action-specific
256
255
 
257
256
  # Check service-specific condition keys (with pattern matching for tag keys)
258
- if service_detail.condition_keys and condition_key_in_list(
259
- condition_key, list(service_detail.condition_keys.keys())
260
- ):
261
- return ConditionKeyValidationResult(is_valid=True)
257
+ # IMPORTANT: aws:RequestTag and aws:ResourceTag patterns in service-level keys
258
+ # are NOT universally valid for all actions. Skip them here - they'll be checked
259
+ # at action/resource level.
260
+ if service_detail.condition_keys:
261
+ # Check if it matches service-level keys, but exclude RequestTag/ResourceTag
262
+ if condition_key_in_list(condition_key, list(service_detail.condition_keys.keys())):
263
+ # If it's RequestTag or ResourceTag, don't return valid here - check action/resource level
264
+ if not (
265
+ condition_key.startswith("aws:RequestTag/")
266
+ or condition_key.startswith("aws:ResourceTag/")
267
+ ):
268
+ return ConditionKeyValidationResult(is_valid=True)
269
+ # For RequestTag/ResourceTag, continue to check action/resource level
262
270
 
263
271
  # Check action-specific condition keys
264
272
  if action_name in service_detail.actions:
@@ -298,8 +306,26 @@ class ServiceValidator:
298
306
  if is_global_key:
299
307
  return ConditionKeyValidationResult(is_valid=True)
300
308
 
301
- # Short error message
302
- error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
309
+ # If we reach here, the condition key was not found in any valid location
310
+ # Check if it's an aws: prefixed key that's not global - provide specific error
311
+ if condition_key.startswith("aws:"):
312
+ # Special handling for aws:RequestTag and aws:ResourceTag patterns
313
+ if condition_key.startswith("aws:RequestTag/"):
314
+ error_msg = (
315
+ f"Condition key `{condition_key}` is not supported by action `{action}`. "
316
+ f"The `aws:RequestTag/${{TagKey}}` condition is only supported by actions that "
317
+ f"create or modify resources with tags. This action does not support tag operations."
318
+ )
319
+ elif condition_key.startswith("aws:ResourceTag/"):
320
+ error_msg = (
321
+ f"Condition key `{condition_key}` is not supported by the resources used by action `{action}`. "
322
+ f"The `aws:ResourceTag/${{TagKey}}` condition is only supported by resources that have tags."
323
+ )
324
+ else:
325
+ error_msg = f"Invalid AWS condition key: `{condition_key}`. This key is not a valid global condition key and is not supported by action `{action}`."
326
+ else:
327
+ # Short error message for non-aws: keys
328
+ error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
303
329
 
304
330
  # Collect valid condition keys for this action
305
331
  valid_keys: set[str] = set()
@@ -14,8 +14,17 @@ Supports:
14
14
 
15
15
  import ipaddress
16
16
  import re
17
+ from datetime import datetime
17
18
  from typing import Any
18
19
 
20
+ # Pre-compiled regex patterns for performance (compiled once at module load)
21
+ # Timezone offset pattern for ISO 8601 dates (e.g., 2025-01-01T12:00:00+00:00)
22
+ _TZ_OFFSET_PATTERN = re.compile(
23
+ r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})"
24
+ r"(?:\.(\d{1,6}))?" # Optional milliseconds/microseconds
25
+ r"([+-])(\d{2}):(\d{2})$" # Timezone offset
26
+ )
27
+
19
28
  # IAM Condition Operators mapped to their expected value types
20
29
  # Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
21
30
  CONDITION_OPERATORS = {
@@ -223,6 +232,163 @@ def validate_value_for_type(value_type: str, values: list[Any]) -> tuple[bool, s
223
232
  return True, None
224
233
 
225
234
 
235
+ def _validate_date_value(value_str: str) -> tuple[bool, str | None]:
236
+ """
237
+ Validate a date value for IAM condition operators.
238
+
239
+ AWS IAM accepts the following date formats:
240
+ 1. ISO 8601 with UTC (Z suffix): 2019-07-16T12:00:00Z
241
+ 2. ISO 8601 with timezone offset: 2019-07-16T12:00:00+00:00
242
+ 3. ISO 8601 with milliseconds: 2019-07-16T12:00:00.000Z
243
+ 4. UNIX epoch timestamp: 1563278400 (seconds since 1970-01-01)
244
+
245
+ This function validates both syntactic correctness AND semantic validity
246
+ (e.g., month must be 1-12, day must be valid for the month).
247
+
248
+ Args:
249
+ value_str: The date string to validate
250
+
251
+ Returns:
252
+ Tuple of (is_valid, error_message)
253
+
254
+ Reference:
255
+ https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_Date
256
+ """
257
+ # Fast path: Check for UNIX epoch timestamp (digits only)
258
+ # Using str.isdigit() is ~3x faster than re.match(r"^\d+$", ...)
259
+ if value_str.isdigit():
260
+ # Validate epoch timestamp is reasonable (not before 1970, not too far in future)
261
+ try:
262
+ epoch = int(value_str)
263
+ # Allow dates from 1970 to year 3000 (covers all reasonable use cases)
264
+ if epoch > 32503680000: # Year 3000 in seconds
265
+ return (
266
+ False,
267
+ f"UNIX epoch timestamp appears unreasonably large: {value_str}. "
268
+ "Expected seconds since 1970-01-01.",
269
+ )
270
+ return True, None
271
+ except (ValueError, OverflowError):
272
+ return (
273
+ False,
274
+ f"Invalid UNIX epoch timestamp: {value_str}. Must be a valid integer.",
275
+ )
276
+
277
+ # Try parsing ISO 8601 formats (ordered by most common first for performance)
278
+ # Using datetime.strptime with try/except is the most efficient approach
279
+ # as it validates both format AND semantic validity (e.g., month 13 is rejected)
280
+ for fmt in (
281
+ "%Y-%m-%dT%H:%M:%SZ", # Basic UTC format (most common)
282
+ "%Y-%m-%dT%H:%M:%S.%fZ", # With milliseconds
283
+ "%Y-%m-%d", # Date only
284
+ ):
285
+ try:
286
+ datetime.strptime(value_str, fmt)
287
+ return True, None
288
+ except ValueError:
289
+ continue
290
+
291
+ # Fall back to timezone offset parsing (e.g., +00:00, -05:00)
292
+ # Uses pre-compiled regex pattern for performance
293
+ match = _TZ_OFFSET_PATTERN.match(value_str)
294
+ if match:
295
+ year, month, day, hour, minute, second = (
296
+ int(match.group(1)),
297
+ int(match.group(2)),
298
+ int(match.group(3)),
299
+ int(match.group(4)),
300
+ int(match.group(5)),
301
+ int(match.group(6)),
302
+ )
303
+ tz_hour, tz_minute = int(match.group(9)), int(match.group(10))
304
+
305
+ # Validate ranges
306
+ validation_error = _validate_datetime_components(
307
+ year, month, day, hour, minute, second, tz_hour, tz_minute
308
+ )
309
+ if validation_error:
310
+ return False, validation_error
311
+
312
+ return True, None
313
+
314
+ # If nothing matched, provide a helpful error
315
+ return (
316
+ False,
317
+ f"Invalid Date value: `{value_str}`. Expected ISO 8601 format "
318
+ "(e.g., `2019-07-16T12:00:00Z`, `2019-07-16T12:00:00+00:00`) "
319
+ "or UNIX epoch timestamp (e.g., `1563278400`).",
320
+ )
321
+
322
+
323
+ def _validate_datetime_components(
324
+ year: int,
325
+ month: int,
326
+ day: int,
327
+ hour: int,
328
+ minute: int,
329
+ second: int,
330
+ tz_hour: int = 0,
331
+ tz_minute: int = 0,
332
+ ) -> str | None:
333
+ """
334
+ Validate individual datetime components for semantic correctness.
335
+
336
+ Args:
337
+ year: Year (1-9999)
338
+ month: Month (1-12)
339
+ day: Day (1-31, depends on month)
340
+ hour: Hour (0-23)
341
+ minute: Minute (0-59)
342
+ second: Second (0-59)
343
+ tz_hour: Timezone hour offset (0-14)
344
+ tz_minute: Timezone minute offset (0-59)
345
+
346
+ Returns:
347
+ Error message string if invalid, None if valid
348
+ """
349
+ # Validate month
350
+ if not 1 <= month <= 12:
351
+ return f"Invalid month: {month}. Must be between 1 and 12."
352
+
353
+ # Validate day based on month
354
+ days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
355
+
356
+ # Handle leap year for February
357
+ is_leap_year = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
358
+ if is_leap_year and month == 2:
359
+ max_day = 29
360
+ else:
361
+ max_day = days_in_month[month]
362
+
363
+ if not 1 <= day <= max_day:
364
+ return f"Invalid day: {day}. For month {month}, day must be between 1 and {max_day}."
365
+
366
+ # Validate hour
367
+ if not 0 <= hour <= 23:
368
+ return f"Invalid hour: {hour}. Must be between 0 and 23."
369
+
370
+ # Validate minute
371
+ if not 0 <= minute <= 59:
372
+ return f"Invalid minute: {minute}. Must be between 0 and 59."
373
+
374
+ # Validate second (allow 59 for leap seconds)
375
+ if not 0 <= second <= 59:
376
+ return f"Invalid second: {second}. Must be between 0 and 59."
377
+
378
+ # Validate timezone offset (max +/- 14:00 per ISO 8601)
379
+ if not 0 <= tz_hour <= 14:
380
+ return f"Invalid timezone hour offset: {tz_hour}. Must be between 0 and 14."
381
+
382
+ if not 0 <= tz_minute <= 59:
383
+ return f"Invalid timezone minute offset: {tz_minute}. Must be between 0 and 59."
384
+
385
+ # Validate that tz offset doesn't exceed 14:00
386
+ if tz_hour == 14 and tz_minute > 0:
387
+ return "Invalid timezone offset. Maximum is +/-14:00."
388
+
389
+ return None
390
+
391
+
226
392
  def _validate_single_value(value_type: str, value_str: str) -> tuple[bool, str | None]:
227
393
  """
228
394
  Validate a single value against its expected type.
@@ -256,15 +422,15 @@ def _validate_single_value(value_type: str, value_str: str) -> tuple[bool, str |
256
422
  return False, f"Expected Bool value (true/false) but got: {value_str}"
257
423
 
258
424
  elif value_type == "Date":
259
- # Date: W3C ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or UNIX epoch timestamp
260
- iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"
261
- epoch_pattern = r"^\d+$"
262
-
263
- if not (re.match(iso_pattern, value_str) or re.match(epoch_pattern, value_str)):
264
- return (
265
- False,
266
- f"Expected Date value (2019-07-16T12:00:00Z or UNIX epoch timestamp) but got: {value_str}",
267
- )
425
+ # Date: W3C ISO 8601 format or UNIX epoch timestamp
426
+ # AWS accepts multiple ISO 8601 variants:
427
+ # - 2019-07-16T12:00:00Z (UTC with Z suffix)
428
+ # - 2019-07-16T12:00:00+00:00 (with timezone offset)
429
+ # - 2019-07-16T12:00:00.000Z (with milliseconds)
430
+ # - UNIX epoch timestamp (seconds since 1970-01-01)
431
+ is_valid_date, date_error = _validate_date_value(value_str)
432
+ if not is_valid_date:
433
+ return False, date_error
268
434
 
269
435
  elif value_type == "IPAddress":
270
436
  # IP Address: IPv4 or IPv6 with optional CIDR notation
@@ -85,17 +85,13 @@ GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS = frozenset(
85
85
  )
86
86
 
87
87
  # Patterns that should be recognized (wildcards and tag-based keys)
88
- # These allow things like aws:RequestTag/Department or aws:PrincipalTag/Environment
88
+ # IMPORTANT: aws:RequestTag and aws:ResourceTag are NOT global condition keys!
89
+ # They are action-specific or resource-specific and must be explicitly listed in
90
+ # the action's ActionConditionKeys or the resource's ConditionKeys.
91
+ # Only aws:PrincipalTag is a true global condition key.
92
+ #
89
93
  # Uses centralized tag key character class from constants
90
94
  AWS_CONDITION_KEY_PATTERNS = [
91
- {
92
- "pattern": rf"^aws:RequestTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
93
- "description": "Tag keys in the request (for tag-based access control)",
94
- },
95
- {
96
- "pattern": rf"^aws:ResourceTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
97
- "description": "Tags on the resource being accessed",
98
- },
99
95
  {
100
96
  "pattern": rf"^aws:PrincipalTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
101
97
  "description": "Tags attached to the principal making the request",
@@ -238,18 +238,26 @@ DEFAULT_CONFIG = {
238
238
  # Applies to: S3 buckets, SNS topics, SQS queues, Lambda functions, etc.
239
239
  # Only runs when: --policy-type RESOURCE_POLICY
240
240
  #
241
- # Three control mechanisms:
242
- # 1. blocked_principals - Block specific principals (deny list)
243
- # 2. allowed_principals - Allow only specific principals (whitelist mode)
244
- # 3. principal_condition_requirements - Require conditions for principals
245
- # 4. allowed_service_principals - Always allow AWS service principals
241
+ # Control mechanisms:
242
+ # 1. block_wildcard_principal - Simple toggle for wildcard principal handling
243
+ # 2. blocked_principals - Block specific principals (deny list)
244
+ # 3. allowed_principals - Allow only specific principals (whitelist mode)
245
+ # 4. principal_condition_requirements - Require conditions for principals
246
+ # 5. allowed_service_principals - Always allow AWS service principals
247
+ # 6. block_service_principal_wildcard - Block {"Service": "*"} patterns
246
248
  "principal_validation": {
247
249
  "enabled": True,
248
250
  "severity": "high", # Security issue, not IAM validity error
249
251
  "description": "Validates Principal elements in resource policies for security best practices",
250
- # blocked_principals: Deny list - these principals are never allowed
251
- # Default: ["*"] blocks public access
252
- "blocked_principals": ["*"],
252
+ # block_wildcard_principal: Strict mode toggle for Principal: "*"
253
+ # false (default): Allow wildcard principal but require conditions
254
+ # true: Block wildcard principal entirely - strictest option
255
+ # When false, principal_condition_requirements for "*" are enforced,
256
+ # allowing patterns like S3 bucket policies with aws:SourceArn conditions.
257
+ "block_wildcard_principal": False,
258
+ # blocked_principals: Deny list - additional principals to block
259
+ # Note: When block_wildcard_principal is true, "*" is automatically blocked.
260
+ "blocked_principals": [],
253
261
  # allowed_principals: Whitelist mode - when set, ONLY these are allowed
254
262
  # Default: [] allows all (except blocked)
255
263
  "allowed_principals": [],
@@ -262,6 +270,12 @@ DEFAULT_CONFIG = {
262
270
  # Default: ["aws:*"] allows ALL AWS service principals
263
271
  # Note: "aws:*" is different from "*" (public access)
264
272
  "allowed_service_principals": ["aws:*"],
273
+ # block_service_principal_wildcard: Block {"Service": "*"} in Principal
274
+ # This pattern allows ANY AWS service to access the resource, which is
275
+ # extremely permissive. Without source verification conditions like
276
+ # aws:SourceArn or aws:SourceAccount, this creates a security risk.
277
+ # Default: True (always block this dangerous pattern)
278
+ "block_service_principal_wildcard": True,
265
279
  },
266
280
  # ========================================================================
267
281
  # 10. TRUST POLICY VALIDATION