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.
- {iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/METADATA +57 -32
- {iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/RECORD +11 -11
- iam_validator/__version__.py +1 -1
- iam_validator/checks/mfa_condition_check.py +50 -1
- iam_validator/checks/not_action_not_resource.py +54 -24
- iam_validator/checks/principal_validation.py +149 -10
- iam_validator/core/condition_validators.py +175 -9
- iam_validator/core/config/defaults.py +22 -8
- {iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iam-policy-validator
|
|
3
|
-
Version: 1.15.
|
|
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
|
-
{
|
|
103
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
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
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
412
|
-
github-summary: true
|
|
413
|
-
fail-on-severity: high
|
|
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"]
|
|
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
|
|
681
|
-
| ------------------------------ |
|
|
682
|
-
| **Primary Purpose** | Pre-deployment validation
|
|
683
|
-
| **Use Case** | CI/CD policy scanning
|
|
684
|
-
| **Custom Security Rules** | ✅ Full support | ❌ 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
|
|
687
|
-
| **Organization Conditions** | ✅ IP, tags, encryption, etc. | ❌ No
|
|
688
|
-
| **CI/CD Ready** | ✅ GitHub Actions native | ⚠️ Manual setup
|
|
689
|
-
| **PR Line Comments** | ✅ Diff-aware | ❌ No
|
|
690
|
-
| **AWS Service Data** | ✅ Official API (auto-update) | ✅ Real AWS account data
|
|
691
|
-
| **Offline Mode** | ✅ Yes | ❌ Needs AWS account
|
|
692
|
-
| **Query Permissions** | ✅ Yes | ✅ Yes (different approach)
|
|
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=
|
|
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=
|
|
12
|
-
iam_validator/checks/not_action_not_resource.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
116
|
-
iam_policy_validator-1.15.
|
|
117
|
-
iam_policy_validator-1.15.
|
|
118
|
-
iam_policy_validator-1.15.
|
|
119
|
-
iam_policy_validator-1.15.
|
|
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,,
|
iam_validator/__version__.py
CHANGED
|
@@ -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.
|
|
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:
|
|
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=
|
|
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: {
|
|
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
|
|
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=
|
|
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: {
|
|
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=
|
|
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
|
|
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: {
|
|
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=
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
#
|
|
242
|
-
# 1.
|
|
243
|
-
# 2.
|
|
244
|
-
# 3.
|
|
245
|
-
# 4.
|
|
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
|
-
#
|
|
251
|
-
#
|
|
252
|
-
|
|
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
|
|
File without changes
|
{iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.15.1.dist-info → iam_policy_validator-1.15.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|