iam-policy-validator 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +18 -19
  2. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/RECORD +31 -20
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +13 -3
  5. iam_validator/checks/action_condition_enforcement.py +1 -6
  6. iam_validator/checks/condition_key_validation.py +21 -1
  7. iam_validator/checks/full_wildcard.py +67 -0
  8. iam_validator/checks/principal_validation.py +497 -3
  9. iam_validator/checks/sensitive_action.py +178 -0
  10. iam_validator/checks/service_wildcard.py +105 -0
  11. iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
  12. iam_validator/checks/wildcard_action.py +62 -0
  13. iam_validator/checks/wildcard_resource.py +131 -0
  14. iam_validator/commands/download_services.py +3 -8
  15. iam_validator/commands/validate.py +28 -2
  16. iam_validator/core/aws_fetcher.py +25 -12
  17. iam_validator/core/check_registry.py +15 -21
  18. iam_validator/core/config/__init__.py +83 -0
  19. iam_validator/core/config/aws_api.py +35 -0
  20. iam_validator/core/config/condition_requirements.py +535 -0
  21. iam_validator/core/config/defaults.py +390 -0
  22. iam_validator/core/config/principal_requirements.py +421 -0
  23. iam_validator/core/config/sensitive_actions.py +133 -0
  24. iam_validator/core/config/service_principals.py +95 -0
  25. iam_validator/core/config/wildcards.py +124 -0
  26. iam_validator/core/config_loader.py +29 -9
  27. iam_validator/core/formatters/enhanced.py +11 -5
  28. iam_validator/core/formatters/sarif.py +78 -14
  29. iam_validator/checks/security_best_practices.py +0 -536
  30. iam_validator/core/defaults.py +0 -393
  31. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
  32. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
  33. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,421 @@
1
+ """
2
+ Principal condition requirement configurations for principal_validation check.
3
+
4
+ This module defines default condition requirements for principals in resource-based
5
+ policies, making it easy to manage complex principal condition enforcement rules
6
+ without deeply nested YAML/dict structures.
7
+
8
+ Using Python provides:
9
+ - Better readability and maintainability
10
+ - Type hints and IDE support
11
+ - Easy to add/modify requirements
12
+ - No parsing overhead
13
+ - Compiled to .pyc
14
+
15
+ Configuration Fields Reference:
16
+ - principals: List of principal patterns to match (supports wildcards)
17
+ - severity: Override default severity for this requirement
18
+ - required_conditions: Conditions that must be present (supports all_of/any_of/none_of)
19
+ - condition_key: The IAM condition key to validate
20
+ - expected_value: (Optional) Expected value for the condition key
21
+ - operator: (Optional) Specific operator to validate (e.g., "StringEquals", "IpAddress")
22
+ - description: Technical description of what the requirement does
23
+ - example: Concrete code example showing proper condition usage
24
+
25
+ Field Progression: detect (condition_key) → explain (description) → demonstrate (example)
26
+
27
+ For detailed explanation of these fields and how to customize requirements,
28
+ see: docs/configuration.md#principal-validation
29
+ """
30
+
31
+ from typing import Any, Final
32
+
33
+ # ============================================================================
34
+ # Principal Condition Requirement Definitions
35
+ # ============================================================================
36
+
37
+ # Public Access (*) - CRITICAL: Must have source restrictions
38
+ PUBLIC_ACCESS_REQUIREMENT: Final[dict[str, Any]] = {
39
+ "principals": ["*"],
40
+ "severity": "critical",
41
+ "required_conditions": {
42
+ "any_of": [
43
+ {
44
+ "condition_key": "aws:SourceArn",
45
+ "description": (
46
+ "Public access must be scoped to a specific source ARN "
47
+ "(e.g., SNS topic, EventBridge rule, CloudFront distribution)"
48
+ ),
49
+ "example": (
50
+ "# Example: S3 bucket policy allowing access from SNS topic\n"
51
+ '"Condition": {\n'
52
+ ' "StringEquals": {\n'
53
+ ' "aws:SourceArn": "arn:aws:sns:us-east-1:123456789012:my-topic"\n'
54
+ " }\n"
55
+ "}"
56
+ ),
57
+ },
58
+ {
59
+ "condition_key": "aws:SourceAccount",
60
+ "description": (
61
+ "Public access must be limited to a specific AWS account to prevent "
62
+ "unauthorized access from other accounts"
63
+ ),
64
+ "example": (
65
+ '"Condition": {\n'
66
+ ' "StringEquals": {\n'
67
+ ' "aws:SourceAccount": "123456789012"\n'
68
+ " }\n"
69
+ "}"
70
+ ),
71
+ },
72
+ {
73
+ "condition_key": "aws:SourceVpce",
74
+ "description": (
75
+ "Or limit public access to a specific VPC endpoint for network-level isolation"
76
+ ),
77
+ "example": (
78
+ '"Condition": {\n'
79
+ ' "StringEquals": {\n'
80
+ ' "aws:SourceVpce": "vpce-1a2b3c4d"\n'
81
+ " }\n"
82
+ "}"
83
+ ),
84
+ },
85
+ {
86
+ "condition_key": "aws:SourceIp",
87
+ "description": ("Or limit public access to specific IP addresses or CIDR ranges"),
88
+ "example": (
89
+ '"Condition": {\n'
90
+ ' "IpAddress": {\n'
91
+ ' "aws:SourceIp": ["10.0.0.0/8", "172.16.0.0/12"]\n'
92
+ " }\n"
93
+ "}"
94
+ ),
95
+ },
96
+ ]
97
+ },
98
+ }
99
+
100
+ # Cross-Account Root Access - HIGH: Must be from same organization
101
+ CROSS_ACCOUNT_ORG_REQUIREMENT: Final[dict[str, Any]] = {
102
+ "principals": ["arn:aws:iam::*:root"],
103
+ "severity": "high",
104
+ "required_conditions": {
105
+ "any_of": [
106
+ {
107
+ "condition_key": "aws:PrincipalOrgID",
108
+ "operator": "StringEquals",
109
+ "description": (
110
+ "Cross-account root access must be from principals in the same AWS Organization "
111
+ "to prevent unauthorized third-party access"
112
+ ),
113
+ "example": (
114
+ "# Replace with your organization ID\n"
115
+ '"Condition": {\n'
116
+ ' "StringEquals": {\n'
117
+ ' "aws:PrincipalOrgID": "o-123456789"\n'
118
+ " }\n"
119
+ "}"
120
+ ),
121
+ },
122
+ {
123
+ "condition_key": "aws:PrincipalOrgPaths",
124
+ "operator": "StringEquals",
125
+ "description": (
126
+ "Cross-account root access must be from principals in the same AWS Organization "
127
+ "to prevent unauthorized third-party access"
128
+ ),
129
+ "example": (
130
+ "# Replace with your organization ID\n"
131
+ '"Condition": {\n'
132
+ ' "StringEquals": {\n'
133
+ ' "aws:PrincipalOrgPaths": "o-123456789/*"\n'
134
+ " }\n"
135
+ "}"
136
+ ),
137
+ },
138
+ ],
139
+ },
140
+ }
141
+
142
+ # IAM Roles - HIGH: Must have MFA or VPC endpoint
143
+ IAM_ROLE_MFA_OR_VPC: Final[dict[str, Any]] = {
144
+ "principals": ["arn:aws:iam::*:role/*"],
145
+ "severity": "high",
146
+ "required_conditions": {
147
+ "any_of": [
148
+ {
149
+ "condition_key": "aws:MultiFactorAuthPresent",
150
+ "expected_value": True,
151
+ "description": "Require MFA authentication for IAM role access",
152
+ "example": (
153
+ '"Condition": {\n "Bool": {\n "aws:MultiFactorAuthPresent": "true"\n }\n}'
154
+ ),
155
+ },
156
+ {
157
+ "condition_key": "aws:SourceVpce",
158
+ "description": (
159
+ "Or require access from a specific VPC endpoint to ensure network-level isolation"
160
+ ),
161
+ "example": (
162
+ '"Condition": {\n'
163
+ ' "StringEquals": {\n'
164
+ ' "aws:SourceVpce": "vpce-12345678"\n'
165
+ " }\n"
166
+ "}"
167
+ ),
168
+ },
169
+ ]
170
+ },
171
+ }
172
+
173
+ # IAM Users - MEDIUM: Must have MFA and IP restrictions
174
+ IAM_USER_MFA_AND_IP: Final[dict[str, Any]] = {
175
+ "principals": ["arn:aws:iam::*:user/*"],
176
+ "severity": "medium",
177
+ "required_conditions": {
178
+ "all_of": [
179
+ {
180
+ "condition_key": "aws:MultiFactorAuthPresent",
181
+ "expected_value": True,
182
+ "severity": "high", # Override for this specific condition
183
+ "description": "IAM users must have MFA enabled for security",
184
+ "example": (
185
+ '"Condition": {\n "Bool": {\n "aws:MultiFactorAuthPresent": "true"\n }\n}'
186
+ ),
187
+ },
188
+ {
189
+ "condition_key": "aws:SourceIp",
190
+ "operator": "IpAddress",
191
+ "description": "IAM users must access from approved corporate IP ranges",
192
+ "example": (
193
+ '"Condition": {\n'
194
+ ' "IpAddress": {\n'
195
+ ' "aws:SourceIp": ["10.0.0.0/8", "172.16.0.0/12"]\n'
196
+ " }\n"
197
+ "}"
198
+ ),
199
+ },
200
+ ]
201
+ },
202
+ }
203
+
204
+ # Federated Users - HIGH: Tag-based access control (ABAC)
205
+ FEDERATED_USER_ABAC: Final[dict[str, Any]] = {
206
+ "principals": ["arn:aws:iam::*:federated-user/*"],
207
+ "severity": "high",
208
+ "required_conditions": {
209
+ "all_of": [
210
+ {
211
+ "condition_key": "aws:PrincipalTag/Department",
212
+ "description": "Federated users must have Department tag for access control",
213
+ "example": (
214
+ '"Condition": {\n'
215
+ ' "StringEquals": {\n'
216
+ ' "aws:PrincipalTag/Department": "Engineering"\n'
217
+ " }\n"
218
+ "}"
219
+ ),
220
+ },
221
+ {
222
+ "condition_key": "aws:RequestTag/Owner",
223
+ "operator": "StringEquals",
224
+ "expected_value": "${aws:PrincipalTag/Owner}",
225
+ "description": (
226
+ "Resource owner must match principal's owner tag (Attribute-Based Access Control)"
227
+ ),
228
+ "example": (
229
+ "# ABAC: Principal tag must match resource tag\n"
230
+ '"Condition": {\n'
231
+ ' "StringEquals": {\n'
232
+ ' "aws:RequestTag/Owner": "${aws:PrincipalTag/Owner}"\n'
233
+ " }\n"
234
+ "}"
235
+ ),
236
+ },
237
+ ]
238
+ },
239
+ }
240
+
241
+ # Prevent Insecure Transport - CRITICAL: Never allow HTTP
242
+ PREVENT_INSECURE_TRANSPORT: Final[dict[str, Any]] = {
243
+ "principals": ["*", "arn:aws:iam::*:*"],
244
+ "severity": "critical",
245
+ "required_conditions": {
246
+ "none_of": [
247
+ {
248
+ "condition_key": "aws:SecureTransport",
249
+ "expected_value": False,
250
+ "description": (
251
+ "Insecure transport (HTTP) must never be explicitly allowed. "
252
+ "This prevents man-in-the-middle attacks and data interception"
253
+ ),
254
+ "example": (
255
+ "# INCORRECT - Never allow this:\n"
256
+ '"Condition": {\n'
257
+ ' "Bool": {\n'
258
+ ' "aws:SecureTransport": "false" # ❌ This is forbidden\n'
259
+ " }\n"
260
+ "}\n\n"
261
+ "# CORRECT - Require HTTPS:\n"
262
+ '"Condition": {\n'
263
+ ' "Bool": {\n'
264
+ ' "aws:SecureTransport": "true" # ✅ Always use this\n'
265
+ " }\n"
266
+ "}"
267
+ ),
268
+ }
269
+ ]
270
+ },
271
+ }
272
+
273
+ # Assumed Role Sessions - MEDIUM: Require session tags
274
+ ASSUMED_ROLE_SESSION_TAGS: Final[dict[str, Any]] = {
275
+ "principals": ["arn:aws:sts::*:assumed-role/*"],
276
+ "severity": "medium",
277
+ "required_conditions": [
278
+ {
279
+ "condition_key": "aws:PrincipalTag/SessionName",
280
+ "description": (
281
+ "Assumed role sessions should be tagged with session name for audit trails "
282
+ "and access attribution"
283
+ ),
284
+ "example": (
285
+ '"Condition": {\n "StringLike": {\n "aws:PrincipalTag/SessionName": "*"\n }\n}'
286
+ ),
287
+ }
288
+ ],
289
+ }
290
+
291
+ # ============================================================================
292
+ # Registry and Helper Functions
293
+ # ============================================================================
294
+
295
+ # All available requirement definitions
296
+ ALL_PRINCIPAL_REQUIREMENTS: Final[dict[str, dict[str, Any]]] = {
297
+ "public_access": PUBLIC_ACCESS_REQUIREMENT,
298
+ "cross_account_org": CROSS_ACCOUNT_ORG_REQUIREMENT,
299
+ "iam_role_mfa_or_vpc": IAM_ROLE_MFA_OR_VPC,
300
+ "iam_user_mfa_and_ip": IAM_USER_MFA_AND_IP,
301
+ "federated_user_abac": FEDERATED_USER_ABAC,
302
+ "prevent_insecure_transport": PREVENT_INSECURE_TRANSPORT,
303
+ "assumed_role_session_tags": ASSUMED_ROLE_SESSION_TAGS,
304
+ }
305
+
306
+ # Default requirements enabled by default (most critical ones)
307
+ DEFAULT_ENABLED_REQUIREMENTS: Final[list[str]] = [
308
+ "public_access", # CRITICAL: Public access must have restrictions
309
+ "prevent_insecure_transport", # CRITICAL: Never allow insecure transport
310
+ ]
311
+
312
+
313
+ def get_default_principal_requirements() -> list[dict[str, Any]]:
314
+ """Get default principal condition requirements (most critical ones enabled by default).
315
+
316
+ Returns:
317
+ List of default principal condition requirements
318
+ """
319
+ return [ALL_PRINCIPAL_REQUIREMENTS[name] for name in DEFAULT_ENABLED_REQUIREMENTS]
320
+
321
+
322
+ def get_principal_requirement(name: str) -> dict[str, Any] | None:
323
+ """Get a single principal condition requirement by name.
324
+
325
+ Args:
326
+ name: The requirement name
327
+
328
+ Returns:
329
+ The principal condition requirement or None if not found
330
+ """
331
+ return ALL_PRINCIPAL_REQUIREMENTS.get(name)
332
+
333
+
334
+ def get_principal_requirements_by_names(names: list[str]) -> list[dict[str, Any]]:
335
+ """Get multiple principal condition requirements by name.
336
+
337
+ Args:
338
+ names: List of requirement names
339
+
340
+ Returns:
341
+ List of principal condition requirements
342
+ """
343
+ return [
344
+ ALL_PRINCIPAL_REQUIREMENTS[name] for name in names if name in ALL_PRINCIPAL_REQUIREMENTS
345
+ ]
346
+
347
+
348
+ def get_principal_requirements_by_severity(severity: str) -> list[dict[str, Any]]:
349
+ """Get principal condition requirements filtered by severity.
350
+
351
+ Args:
352
+ severity: The severity level (critical, high, medium, low)
353
+
354
+ Returns:
355
+ List of principal condition requirements with matching severity
356
+ """
357
+ return [req for req in ALL_PRINCIPAL_REQUIREMENTS.values() if req.get("severity") == severity]
358
+
359
+
360
+ def get_all_principal_requirement_names() -> list[str]:
361
+ """Get all available principal condition requirement names.
362
+
363
+ Returns:
364
+ List of all requirement names
365
+ """
366
+ return list(ALL_PRINCIPAL_REQUIREMENTS.keys())
367
+
368
+
369
+ # ============================================================================
370
+ # Usage Examples
371
+ # ============================================================================
372
+
373
+ """
374
+ # Example 1: Use default requirements (public_access + prevent_insecure_transport)
375
+ from iam_validator.core.config import get_default_principal_requirements
376
+
377
+ config = {
378
+ "principal_validation": {
379
+ "enabled": True,
380
+ "principal_condition_requirements": get_default_principal_requirements(),
381
+ }
382
+ }
383
+
384
+ # Example 2: Use specific requirements by name
385
+ from iam_validator.core.config import get_principal_requirements_by_names
386
+
387
+ config = {
388
+ "principal_validation": {
389
+ "enabled": True,
390
+ "principal_condition_requirements": get_principal_requirements_by_names([
391
+ "public_access",
392
+ "cross_account_org",
393
+ "iam_role_mfa_or_vpc",
394
+ ]),
395
+ }
396
+ }
397
+
398
+ # Example 3: Get all critical severity requirements
399
+ from iam_validator.core.config import get_principal_requirements_by_severity
400
+
401
+ config = {
402
+ "principal_validation": {
403
+ "enabled": True,
404
+ "principal_condition_requirements": get_principal_requirements_by_severity("critical"),
405
+ }
406
+ }
407
+
408
+ # Example 4: Get all available requirement names
409
+ from iam_validator.core.config import get_all_principal_requirement_names
410
+
411
+ print(get_all_principal_requirement_names())
412
+ # Output: ['public_access', 'cross_account_org', 'iam_role_mfa_or_vpc', ...]
413
+
414
+ # Example 5: Get a single requirement
415
+ from iam_validator.core.config import get_principal_requirement
416
+
417
+ req = get_principal_requirement("public_access")
418
+ if req:
419
+ print(req["principals"]) # ["*"]
420
+ print(req["severity"]) # "critical"
421
+ """
@@ -0,0 +1,133 @@
1
+ """
2
+ Sensitive actions catalog for IAM Policy Validator.
3
+
4
+ This module defines a curated list of sensitive AWS actions that should
5
+ typically have IAM conditions to limit when they can be used.
6
+
7
+ Using Python instead of YAML/JSON provides:
8
+ - Zero parsing overhead (compiled to .pyc)
9
+ - Better PyPI packaging (no data files)
10
+ - Type hints and IDE autocomplete
11
+ - Easy to extend and maintain
12
+
13
+ The list is a frozenset for O(1) lookup performance.
14
+ """
15
+
16
+ from typing import Final
17
+
18
+ # ============================================================================
19
+ # Sensitive Actions List
20
+ # ============================================================================
21
+ # These actions are considered sensitive and should typically have IAM
22
+ # conditions to limit when they can be used. Examples include:
23
+ # - IAM identity and access management changes
24
+ # - Secrets and credential operations
25
+ # - Destructive operations (deletions)
26
+ # - Security configuration changes
27
+ # - Network and firewall modifications
28
+ # - Logging and audit trail changes
29
+ # ============================================================================
30
+
31
+ DEFAULT_SENSITIVE_ACTIONS: Final[frozenset[str]] = frozenset(
32
+ {
33
+ # IAM & Identity Management
34
+ "iam:AddClientIDToOpenIDConnectProvider",
35
+ "iam:AttachRolePolicy",
36
+ "iam:AttachUserPolicy",
37
+ "iam:CreateAccessKey",
38
+ "iam:CreateOpenIDConnectProvider",
39
+ "iam:CreatePolicyVersion",
40
+ "iam:CreateRole",
41
+ "iam:CreateSAMLProvider",
42
+ "iam:CreateUser",
43
+ "iam:DeleteAccessKey",
44
+ "iam:DeleteLoginProfile",
45
+ "iam:DeleteOpenIDConnectProvider",
46
+ "iam:DeleteRole",
47
+ "iam:DeleteRolePolicy",
48
+ "iam:DeleteSAMLProvider",
49
+ "iam:DeleteUser",
50
+ "iam:DeleteUserPolicy",
51
+ "iam:DetachRolePolicy",
52
+ "iam:DetachUserPolicy",
53
+ "iam:PutRolePolicy",
54
+ "iam:PutUserPolicy",
55
+ "iam:SetDefaultPolicyVersion",
56
+ "iam:UpdateAccessKey",
57
+ "iam:UpdateAssumeRolePolicy",
58
+ # Secrets & Credentials
59
+ "kms:DisableKey",
60
+ "kms:PutKeyPolicy",
61
+ "kms:ScheduleKeyDeletion",
62
+ "secretsmanager:DeleteSecret",
63
+ "secretsmanager:GetSecretValue",
64
+ "secretsmanager:PutSecretValue",
65
+ "ssm:DeleteParameter",
66
+ "ssm:PutParameter",
67
+ # Compute & Containers
68
+ "ec2:DeleteSnapshot",
69
+ "ec2:DeleteVolume",
70
+ "ec2:DeleteVpc",
71
+ "ec2:ModifyInstanceAttribute",
72
+ "ec2:TerminateInstances",
73
+ "ecr:DeleteRepository",
74
+ "ecs:DeleteCluster",
75
+ "ecs:DeleteService",
76
+ "eks:DeleteCluster",
77
+ "lambda:DeleteFunction",
78
+ "lambda:DeleteFunctionConcurrency",
79
+ "lambda:PutFunctionConcurrency",
80
+ # Database & Storage
81
+ "dynamodb:DeleteTable",
82
+ "efs:DeleteFileSystem",
83
+ "elasticache:DeleteCacheCluster",
84
+ "fsx:DeleteFileSystem",
85
+ "rds:DeleteDBCluster",
86
+ "rds:DeleteDBInstance",
87
+ "redshift:DeleteCluster",
88
+ # S3 & Backup
89
+ "backup:DeleteBackupVault",
90
+ "glacier:DeleteArchive",
91
+ "s3:DeleteBucket",
92
+ "s3:DeleteBucketPolicy",
93
+ "s3:DeleteObject",
94
+ "s3:PutBucketPolicy",
95
+ "s3:PutLifecycleConfiguration",
96
+ # Network & Security
97
+ "ec2:ApplySecurityGroupsToClientVpnTargetNetwork",
98
+ "ec2:AssociateClientVpnTargetNetwork",
99
+ "ec2:AssociateSecurityGroupVpc",
100
+ "ec2:AttachVpnGateway",
101
+ "ec2:AuthorizeClientVpnIngress",
102
+ "ec2:AuthorizeSecurityGroupIngress",
103
+ "ec2:CreateClientVpnRoute",
104
+ "ec2:CreateVpnConnection",
105
+ "ec2:DeleteSecurityGroup",
106
+ "ec2:DeleteVpnConnection",
107
+ "ec2:DisassociateRouteTable",
108
+ "ec2:RevokeSecurityGroupEgress",
109
+ # Access & Logging
110
+ "cloudtrail:DeleteTrail",
111
+ "cloudtrail:StopLogging",
112
+ "cloudwatch:DeleteLogGroup",
113
+ "config:DeleteConfigurationRecorder",
114
+ "guardduty:DeleteDetector",
115
+ # Account & Organization
116
+ "account:CloseAccount",
117
+ "account:CreateAccount",
118
+ "account:DeleteAccount",
119
+ "organizations:DeleteOrganization",
120
+ "organizations:LeaveOrganization",
121
+ "organizations:RemoveAccountFromOrganization",
122
+ }
123
+ )
124
+
125
+
126
+ def get_sensitive_actions() -> frozenset[str]:
127
+ """
128
+ Get all sensitive actions.
129
+
130
+ Returns:
131
+ Frozenset of all sensitive actions for O(1) membership testing
132
+ """
133
+ return DEFAULT_SENSITIVE_ACTIONS
@@ -0,0 +1,95 @@
1
+ """
2
+ Default service principals for resource policy validation.
3
+
4
+ These are AWS service principals that are commonly used and considered safe
5
+ in resource-based policies (S3 bucket policies, SNS topic policies, etc.).
6
+ """
7
+
8
+ from typing import Final
9
+
10
+ # ============================================================================
11
+ # Allowed Service Principals
12
+ # ============================================================================
13
+ # These AWS service principals are commonly used in resource policies
14
+ # and are generally considered safe to allow
15
+
16
+ DEFAULT_SERVICE_PRINCIPALS: Final[tuple[str, ...]] = (
17
+ "cloudfront.amazonaws.com",
18
+ "s3.amazonaws.com",
19
+ "sns.amazonaws.com",
20
+ "lambda.amazonaws.com",
21
+ "logs.amazonaws.com",
22
+ "events.amazonaws.com",
23
+ "elasticloadbalancing.amazonaws.com",
24
+ "cloudtrail.amazonaws.com",
25
+ "config.amazonaws.com",
26
+ "backup.amazonaws.com",
27
+ "cloudwatch.amazonaws.com",
28
+ "monitoring.amazonaws.com",
29
+ "ec2.amazonaws.com",
30
+ "ecs-tasks.amazonaws.com",
31
+ "eks.amazonaws.com",
32
+ "apigateway.amazonaws.com",
33
+ )
34
+
35
+
36
+ def get_service_principals() -> tuple[str, ...]:
37
+ """
38
+ Get tuple of allowed service principals.
39
+
40
+ Returns:
41
+ Tuple of AWS service principal names
42
+ """
43
+ return DEFAULT_SERVICE_PRINCIPALS
44
+
45
+
46
+ def is_allowed_service_principal(principal: str) -> bool:
47
+ """
48
+ Check if a principal is an allowed service principal.
49
+
50
+ Args:
51
+ principal: Principal to check (e.g., "lambda.amazonaws.com")
52
+
53
+ Returns:
54
+ True if principal is in allowed list
55
+
56
+ Performance: O(n) but small list (~16 items)
57
+ """
58
+ return principal in DEFAULT_SERVICE_PRINCIPALS
59
+
60
+
61
+ def get_service_principals_by_category() -> dict[str, tuple[str, ...]]:
62
+ """
63
+ Get service principals organized by service category.
64
+
65
+ Returns:
66
+ Dictionary mapping categories to service principal tuples
67
+ """
68
+ return {
69
+ "storage": (
70
+ "s3.amazonaws.com",
71
+ "backup.amazonaws.com",
72
+ ),
73
+ "compute": (
74
+ "lambda.amazonaws.com",
75
+ "ec2.amazonaws.com",
76
+ "ecs-tasks.amazonaws.com",
77
+ "eks.amazonaws.com",
78
+ ),
79
+ "networking": (
80
+ "cloudfront.amazonaws.com",
81
+ "elasticloadbalancing.amazonaws.com",
82
+ "apigateway.amazonaws.com",
83
+ ),
84
+ "monitoring": (
85
+ "logs.amazonaws.com",
86
+ "cloudwatch.amazonaws.com",
87
+ "monitoring.amazonaws.com",
88
+ "cloudtrail.amazonaws.com",
89
+ ),
90
+ "messaging": (
91
+ "sns.amazonaws.com",
92
+ "events.amazonaws.com",
93
+ ),
94
+ "management": ("config.amazonaws.com",),
95
+ }