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.
- {iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.dist-info}/METADATA +57 -32
- {iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.dist-info}/RECORD +11 -11
- iam_validator/__version__.py +1 -1
- iam_validator/checks/principal_validation.py +149 -10
- iam_validator/core/aws_service/validators.py +37 -11
- iam_validator/core/condition_validators.py +175 -9
- iam_validator/core/config/aws_global_conditions.py +5 -9
- iam_validator/core/config/defaults.py +22 -8
- {iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.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.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
|
-
{
|
|
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=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=
|
|
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
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.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,,
|
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.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
|
|
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:
|
|
@@ -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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
#
|
|
302
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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.0.dist-info → iam_policy_validator-1.15.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.15.0.dist-info → iam_policy_validator-1.15.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|