iam-policy-validator 1.15.1__py3-none-any.whl → 1.15.3__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.1
3
+ Version: 1.15.3
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=3sYnVKt3REhH-Klaa8pLaSXpDxcV1dV0Se9CjHHE4Nc,374
3
+ iam_validator/__version__.py,sha256=2oTvvCnwbVqgXWNdEDQWe8-xj2ecUXJtl1jF_CIIGxk,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
@@ -8,12 +8,12 @@ iam_validator/checks/action_validation.py,sha256=gy9_mujYbojBboLk0WwuJYJjggrY2sR
8
8
  iam_validator/checks/condition_key_validation.py,sha256=5i8LqqV78SjWK6pLrbttWmeMAD4pDC12_FjTjx5dFSU,4024
9
9
  iam_validator/checks/condition_type_mismatch.py,sha256=KJp7zQHDd8VeTcfjcD-ur3S4070cXEDTWkFtxfp7CuE,10652
10
10
  iam_validator/checks/full_wildcard.py,sha256=0TkkHtV0MZ6nZtJRtGdn3wwOMM96TRyGO7l7mmdHNUo,2325
11
- iam_validator/checks/mfa_condition_check.py,sha256=y1LbqcvQ_fL2BPTNaKRQoBYM5hM7JET9cDPUOWKFEVs,4814
12
- iam_validator/checks/not_action_not_resource.py,sha256=WWKOCLCq7yxOG9tgi1n5xPpphTLZ9RfcfPwZ-TP6n9Y,8097
11
+ iam_validator/checks/mfa_condition_check.py,sha256=EYOzESzsZVlxW9Pp6hSqPZkH0UT1h21SoZibBX7k_OU,7705
12
+ iam_validator/checks/not_action_not_resource.py,sha256=18wB1NKaCj4X7o8xcTLmtFjsXFJLXTukzp1FhZzvMa8,9779
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
@@ -72,7 +72,7 @@ iam_validator/core/config/category_suggestions.py,sha256=fopaZ9kXDrsLgi_r0pERrLw
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.1.dist-info/METADATA,sha256=5U2vmBBtv-GXPRTflISPL-sOmq_kT9aWlDdOBsfbMnI,34808
116
- iam_policy_validator-1.15.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
117
- iam_policy_validator-1.15.1.dist-info/entry_points.txt,sha256=VXAcx1evo9fuxX0Gtj3J2HnzWcBHSXugiZwBtQ1BXE0,162
118
- iam_policy_validator-1.15.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
119
- iam_policy_validator-1.15.1.dist-info/RECORD,,
115
+ iam_policy_validator-1.15.3.dist-info/METADATA,sha256=r1dXILWhINVsEMyiTTC3IJpAEAoS0usdkBkt7lVe1og,34939
116
+ iam_policy_validator-1.15.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
117
+ iam_policy_validator-1.15.3.dist-info/entry_points.txt,sha256=VXAcx1evo9fuxX0Gtj3J2HnzWcBHSXugiZwBtQ1BXE0,162
118
+ iam_policy_validator-1.15.3.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
119
+ iam_policy_validator-1.15.3.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.1"
6
+ __version__ = "1.15.3"
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("."))
@@ -75,7 +75,36 @@ class MFAConditionCheck(PolicyCheck):
75
75
  )
76
76
  )
77
77
 
78
- # Check for anti-pattern #2: Null with aws:MultiFactorAuthPresent = false
78
+ # Check for anti-pattern #2: BoolIfExists with aws:MultiFactorAuthPresent = false
79
+ # This is MORE dangerous than Bool because it also matches when the key is missing
80
+ bool_if_exists_conditions = statement.condition.get("BoolIfExists", {})
81
+ for key, value in bool_if_exists_conditions.items():
82
+ if key.lower() == "aws:multifactorauthpresent":
83
+ # Normalize value to list
84
+ values = value if isinstance(value, list) else [value]
85
+ # Convert to lowercase strings for comparison
86
+ values_lower = [str(v).lower() for v in values]
87
+
88
+ if "false" in values_lower or False in values:
89
+ issues.append(
90
+ ValidationIssue(
91
+ severity="high", # Higher than default - this is worse than Bool
92
+ message=(
93
+ "**DANGEROUS MFA condition pattern detected.** "
94
+ 'Using `{"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}` '
95
+ "is MORE dangerous than using `Bool` because it also matches when "
96
+ "the key is missing entirely (no MFA context in the request). "
97
+ "This effectively allows access without any MFA verification."
98
+ ),
99
+ statement_sid=statement_sid,
100
+ statement_index=statement_idx,
101
+ issue_type="mfa_antipattern_boolif_exists_false",
102
+ line_number=line_number,
103
+ field_name="condition",
104
+ )
105
+ )
106
+
107
+ # Check for anti-pattern #3: Null with aws:MultiFactorAuthPresent = false
79
108
  null_conditions = statement.condition.get("Null", {})
80
109
  for key, value in null_conditions.items():
81
110
  if key.lower() == "aws:multifactorauthpresent":
@@ -102,4 +131,24 @@ class MFAConditionCheck(PolicyCheck):
102
131
  )
103
132
  )
104
133
 
134
+ # Check for anti-pattern #4: Null with aws:MultiFactorAuthPresent = true
135
+ # This means "key does NOT exist" = no MFA was used
136
+ if "true" in values_lower or True in values:
137
+ issues.append(
138
+ ValidationIssue(
139
+ severity=self.get_severity(config),
140
+ message=(
141
+ "**Dangerous MFA condition pattern detected.** "
142
+ 'Using `{"Null": {"aws:MultiFactorAuthPresent": "true"}}` checks if the key '
143
+ "does NOT exist, which means no MFA was provided in the request context. "
144
+ "This condition allows access when MFA is absent."
145
+ ),
146
+ statement_sid=statement_sid,
147
+ statement_index=statement_idx,
148
+ issue_type="mfa_antipattern_null_true",
149
+ line_number=line_number,
150
+ field_name="condition",
151
+ )
152
+ )
153
+
105
154
  return issues
@@ -12,6 +12,13 @@ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
12
12
  from iam_validator.core.models import Statement, ValidationIssue
13
13
 
14
14
 
15
+ def _format_list_with_backticks(items: list[str], max_items: int = 3) -> str:
16
+ """Format a list of items with backticks for markdown rendering."""
17
+ formatted = [f"`{item}`" for item in items[:max_items]]
18
+ suffix = "..." if len(items) > max_items else ""
19
+ return ", ".join(formatted) + suffix
20
+
21
+
15
22
  class NotActionNotResourceCheck(PolicyCheck):
16
23
  """Checks for dangerous NotAction/NotResource patterns.
17
24
 
@@ -59,18 +66,18 @@ class NotActionNotResourceCheck(PolicyCheck):
59
66
  statement_index=statement_idx,
60
67
  issue_type="not_action_allow",
61
68
  message=(
62
- "Statement uses NotAction with Allow effect. "
69
+ "Statement uses `NotAction` with `Allow` effect. "
63
70
  "This grants ALL actions except the listed ones. "
64
71
  "While conditions are present, this pattern is still risky."
65
72
  ),
66
73
  suggestion=(
67
- "Consider using explicit Action lists instead of NotAction. "
68
- "If NotAction is required, ensure conditions are comprehensive."
74
+ "Consider using explicit `Action` lists instead of `NotAction`. "
75
+ "If `NotAction` is required, ensure conditions are comprehensive."
69
76
  ),
70
77
  example='{\n "Effect": "Allow",\n "Action": ["s3:GetObject", "s3:ListBucket"],\n "Resource": "*"\n}',
71
78
  line_number=statement.line_number,
72
79
  field_name="action",
73
- action=", ".join(not_actions[:3]) + ("..." if len(not_actions) > 3 else ""),
80
+ action=_format_list_with_backticks(not_actions, 3),
74
81
  )
75
82
  )
76
83
  else:
@@ -82,20 +89,19 @@ class NotActionNotResourceCheck(PolicyCheck):
82
89
  statement_index=statement_idx,
83
90
  issue_type="not_action_allow_no_condition",
84
91
  message=(
85
- "Statement uses NotAction with Allow effect and NO conditions. "
86
- f"This grants ALL AWS actions except: {', '.join(not_actions[:5])}"
87
- f"{'...' if len(not_actions) > 5 else ''}. "
92
+ "Statement uses `NotAction` with `Allow` effect and NO conditions. "
93
+ f"This grants ALL AWS actions except: {_format_list_with_backticks(not_actions, 5)}. "
88
94
  "This is equivalent to granting near-administrator access."
89
95
  ),
90
96
  suggestion=(
91
- "Replace NotAction with explicit Action list. "
92
- "If NotAction is required, add strict conditions like "
93
- "aws:SourceIp, aws:PrincipalArn, or aws:MultiFactorAuthPresent."
97
+ "Replace `NotAction` with explicit `Action` list. "
98
+ "If `NotAction` is required, add strict conditions like "
99
+ "`aws:SourceIp`, `aws:PrincipalArn`, or `aws:MultiFactorAuthPresent`."
94
100
  ),
95
101
  example='{\n "Effect": "Allow",\n "Action": ["specific:Action"],\n "Resource": "*",\n "Condition": {\n "Bool": {"aws:MultiFactorAuthPresent": "true"}\n }\n}',
96
102
  line_number=statement.line_number,
97
103
  field_name="action",
98
- action=", ".join(not_actions[:3]) + ("..." if len(not_actions) > 3 else ""),
104
+ action=_format_list_with_backticks(not_actions, 3),
99
105
  )
100
106
  )
101
107
 
@@ -113,24 +119,49 @@ class NotActionNotResourceCheck(PolicyCheck):
113
119
  statement_index=statement_idx,
114
120
  issue_type="not_resource_broad",
115
121
  message=(
116
- "Statement uses NotResource with Allow effect and broad Resource. "
117
- f"This grants access to ALL resources except: {', '.join(not_resources[:3])}"
118
- f"{'...' if len(not_resources) > 3 else ''}."
122
+ "Statement uses `NotResource` with `Allow` effect and broad `Resource`. "
123
+ f"This grants access to ALL resources except: {_format_list_with_backticks(not_resources, 3)}."
119
124
  ),
120
125
  suggestion=(
121
- "Replace NotResource with explicit Resource ARNs. "
122
- "Using NotResource grants access to all current and future resources "
126
+ "Replace `NotResource` with explicit `Resource` ARNs. "
127
+ "Using `NotResource` grants access to all current and future resources "
123
128
  "except those explicitly excluded."
124
129
  ),
125
130
  example='{\n "Effect": "Allow",\n "Action": ["s3:GetObject"],\n "Resource": "arn:aws:s3:::my-bucket/*"\n}',
126
131
  line_number=statement.line_number,
127
132
  field_name="resource",
128
- resource=", ".join(not_resources[:3])
129
- + ("..." if len(not_resources) > 3 else ""),
133
+ resource=_format_list_with_backticks(not_resources, 3),
130
134
  )
131
135
  )
132
136
 
133
- # Check 3: NotAction with Deny - less dangerous but worth noting
137
+ # Check 3: Combined NotAction AND NotResource with Allow
138
+ # This is the most dangerous pattern - grants nearly all actions on nearly all resources
139
+ if not_actions and not_resources and effect == "Allow":
140
+ issues.append(
141
+ ValidationIssue(
142
+ severity="critical",
143
+ statement_sid=statement.sid,
144
+ statement_index=statement_idx,
145
+ issue_type="combined_not_action_not_resource",
146
+ message=(
147
+ "**CRITICAL:** Policy uses both `NotAction` AND `NotResource` with `Allow` effect. "
148
+ f"This grants ALL actions except [{_format_list_with_backticks(not_actions, 3)}] "
149
+ f"on ALL resources except [{_format_list_with_backticks(not_resources, 3)}]. "
150
+ "This is equivalent to near-administrator access."
151
+ ),
152
+ suggestion=(
153
+ "Rewrite using explicit `Action` and `Resource` lists instead of negations. "
154
+ "The combination of `NotAction` + `NotResource` is almost always a mistake."
155
+ ),
156
+ example='{\n "Effect": "Allow",\n "Action": ["specific:Action"],\n "Resource": "arn:aws:service:::specific-resource"\n}',
157
+ line_number=statement.line_number,
158
+ field_name="action",
159
+ action=_format_list_with_backticks(not_actions, 3),
160
+ resource=_format_list_with_backticks(not_resources, 3),
161
+ )
162
+ )
163
+
164
+ # Check 4: NotAction with Deny - less dangerous but worth noting
134
165
  # NotAction with Deny means "deny everything except these actions"
135
166
  # which is actually a valid deny pattern but should be reviewed
136
167
  if not_actions and effect == "Deny":
@@ -145,18 +176,17 @@ class NotActionNotResourceCheck(PolicyCheck):
145
176
  statement_index=statement_idx,
146
177
  issue_type="not_action_deny_review",
147
178
  message=(
148
- "Statement uses NotAction with Deny effect on all resources. "
149
- f"This denies everything except: {', '.join(not_actions[:5])}"
150
- f"{'...' if len(not_actions) > 5 else ''}. "
179
+ "Statement uses `NotAction` with `Deny` effect on all resources. "
180
+ f"This denies everything except: {_format_list_with_backticks(not_actions, 5)}. "
151
181
  "Review to ensure this is the intended behavior."
152
182
  ),
153
183
  suggestion=(
154
184
  "Verify that allowing only these specific actions is intended. "
155
- "Consider if an explicit Allow list would be clearer."
185
+ "Consider if an explicit `Allow` list would be clearer."
156
186
  ),
157
187
  line_number=statement.line_number,
158
188
  field_name="action",
159
- action=", ".join(not_actions[:3]) + ("..." if len(not_actions) > 3 else ""),
189
+ action=_format_list_with_backticks(not_actions, 3),
160
190
  )
161
191
  )
162
192
 
@@ -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:
@@ -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
@@ -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