@actsecurity/iam-lens 0.1.163 → 0.1.164
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.
- package/README.md +30 -1
- package/dist/cjs/cli.js +38 -15
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/principals.d.ts +1 -0
- package/dist/cjs/principals.d.ts.map +1 -1
- package/dist/cjs/principals.js +5 -2
- package/dist/cjs/principals.js.map +1 -1
- package/dist/cjs/simulate/cliVariant.d.ts +116 -0
- package/dist/cjs/simulate/cliVariant.d.ts.map +1 -0
- package/dist/cjs/simulate/cliVariant.js +87 -0
- package/dist/cjs/simulate/cliVariant.js.map +1 -0
- package/dist/cjs/simulate/contextKeys.d.ts +23 -2
- package/dist/cjs/simulate/contextKeys.d.ts.map +1 -1
- package/dist/cjs/simulate/contextKeys.js +41 -11
- package/dist/cjs/simulate/contextKeys.js.map +1 -1
- package/dist/cjs/simulate/generatedPolicies.d.ts +23 -0
- package/dist/cjs/simulate/generatedPolicies.d.ts.map +1 -0
- package/dist/cjs/simulate/generatedPolicies.js +60 -0
- package/dist/cjs/simulate/generatedPolicies.js.map +1 -0
- package/dist/cjs/simulate/simulate.d.ts +107 -21
- package/dist/cjs/simulate/simulate.d.ts.map +1 -1
- package/dist/cjs/simulate/simulate.js +351 -190
- package/dist/cjs/simulate/simulate.js.map +1 -1
- package/dist/esm/cli.js +39 -16
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/principals.d.ts +1 -0
- package/dist/esm/principals.d.ts.map +1 -1
- package/dist/esm/principals.js +5 -2
- package/dist/esm/principals.js.map +1 -1
- package/dist/esm/simulate/cliVariant.d.ts +116 -0
- package/dist/esm/simulate/cliVariant.d.ts.map +1 -0
- package/dist/esm/simulate/cliVariant.js +84 -0
- package/dist/esm/simulate/cliVariant.js.map +1 -0
- package/dist/esm/simulate/contextKeys.d.ts +23 -2
- package/dist/esm/simulate/contextKeys.d.ts.map +1 -1
- package/dist/esm/simulate/contextKeys.js +40 -12
- package/dist/esm/simulate/contextKeys.js.map +1 -1
- package/dist/esm/simulate/generatedPolicies.d.ts +23 -0
- package/dist/esm/simulate/generatedPolicies.d.ts.map +1 -0
- package/dist/esm/simulate/generatedPolicies.js +56 -0
- package/dist/esm/simulate/generatedPolicies.js.map +1 -0
- package/dist/esm/simulate/simulate.d.ts +107 -21
- package/dist/esm/simulate/simulate.d.ts.map +1 -1
- package/dist/esm/simulate/simulate.js +350 -192
- package/dist/esm/simulate/simulate.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.simulateRequest = simulateRequest;
|
|
4
|
+
exports.simulateAnonymousRequest = simulateAnonymousRequest;
|
|
5
|
+
exports.simulateExternalPrincipalRequest = simulateExternalPrincipalRequest;
|
|
6
|
+
exports.simulateExternalResourceRequest = simulateExternalResourceRequest;
|
|
4
7
|
exports.discoveryConstraintForStrictKey = discoveryConstraintForStrictKey;
|
|
5
8
|
exports.resultMatchesExpectation = resultMatchesExpectation;
|
|
6
9
|
const iam_data_1 = require("@actsecurity/iam-data");
|
|
@@ -10,25 +13,88 @@ const principals_js_1 = require("../principals.js");
|
|
|
10
13
|
const resources_js_1 = require("../resources.js");
|
|
11
14
|
const sts_js_1 = require("../utils/sts.js");
|
|
12
15
|
const contextKeys_js_1 = require("./contextKeys.js");
|
|
16
|
+
const generatedPolicies_js_1 = require("./generatedPolicies.js");
|
|
13
17
|
const kmsRetireGrantAction = 'kms:retiregrant';
|
|
14
18
|
/**
|
|
15
|
-
* Simulate an IAM request
|
|
19
|
+
* Simulate an IAM request using the collected policies for both the principal and resource.
|
|
16
20
|
*
|
|
17
|
-
* @param simulationRequest the simulation request details
|
|
18
|
-
* @param collectClient the IAM collect client to use for data access
|
|
19
|
-
* @returns the
|
|
21
|
+
* @param simulationRequest the simulation request details
|
|
22
|
+
* @param collectClient the IAM collect client to use for data access
|
|
23
|
+
* @returns the normalized request and simulation result
|
|
20
24
|
*/
|
|
21
25
|
async function simulateRequest(simulationRequest, collectClient) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
return simulateRequestInternal({ ...simulationRequest }, 'normal', collectClient);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Simulate an unsigned request against a concrete resource ARN.
|
|
30
|
+
*
|
|
31
|
+
* @param simulationRequest the anonymous request details
|
|
32
|
+
* @param collectClient the IAM collect client to use for data access
|
|
33
|
+
* @returns the normalized anonymous request and simulation result
|
|
34
|
+
*/
|
|
35
|
+
async function simulateAnonymousRequest(simulationRequest, collectClient) {
|
|
36
|
+
return simulateRequestInternal({ ...simulationRequest, principal: undefined }, 'anonymous', collectClient);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Simulate a request from an uncollected principal that has a synthetic administrator identity policy.
|
|
40
|
+
* SCPs and permission boundaries do not apply, while resource-side controls still apply.
|
|
41
|
+
* Wildcard-only actions are unsupported.
|
|
42
|
+
*
|
|
43
|
+
* @param simulationRequest the external principal request details
|
|
44
|
+
* @param collectClient the IAM collect client to use for resource-side data access
|
|
45
|
+
* @returns the normalized request and simulation result
|
|
46
|
+
*/
|
|
47
|
+
async function simulateExternalPrincipalRequest(simulationRequest, collectClient) {
|
|
48
|
+
return simulateRequestInternal({ ...simulationRequest }, 'externalPrincipal', collectClient);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Simulate a collected principal accessing a resource outside the collected dataset.
|
|
52
|
+
* Wildcard-only actions are unsupported.
|
|
53
|
+
*
|
|
54
|
+
* @param simulationRequest the external resource request and optional resource/trust policy
|
|
55
|
+
* @param collectClient the IAM collect client to use for principal-side data access
|
|
56
|
+
* @returns the normalized request and simulation result
|
|
57
|
+
*/
|
|
58
|
+
async function simulateExternalResourceRequest(simulationRequest, collectClient) {
|
|
59
|
+
return simulateRequestInternal({ ...simulationRequest }, 'externalResource', collectClient);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Assemble and run every simulation variant through one policy and context processing path.
|
|
63
|
+
*
|
|
64
|
+
* @param input the normalized caller input
|
|
65
|
+
* @param variant the policy-source behavior selected by the public entry point
|
|
66
|
+
* @param collectClient the IAM collect client used for applicable data lookups
|
|
67
|
+
* @returns the normalized request and simulation result
|
|
68
|
+
*/
|
|
69
|
+
async function simulateRequestInternal(input, variant, collectClient) {
|
|
70
|
+
const simulationRequest = { ...input };
|
|
71
|
+
if (variant === 'anonymous') {
|
|
72
|
+
const invalidContextKey = Object.keys(simulationRequest.customContextKeys).find(contextKeys_js_1.isAnonymousPrincipalContextKey);
|
|
73
|
+
if (invalidContextKey) {
|
|
74
|
+
throw new Error(`Anonymous simulations cannot specify principal-derived context key ${invalidContextKey}.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const [service, serviceAction] = simulationRequest.action.split(':');
|
|
25
78
|
const serviceExists = await (0, iam_data_1.iamServiceExists)(service);
|
|
26
79
|
const actionExists = serviceExists && (await (0, iam_data_1.iamActionExists)(service, serviceAction));
|
|
27
80
|
if (!serviceExists || !actionExists) {
|
|
28
81
|
throw new Error(`Unable to find action details for ${simulationRequest.action}`);
|
|
29
82
|
}
|
|
30
83
|
const actionDetails = await (0, iam_data_1.iamActionDetails)(service, serviceAction);
|
|
31
|
-
|
|
84
|
+
if (actionDetails.isWildcardOnly && variant !== 'normal') {
|
|
85
|
+
const variantName = variant === 'anonymous'
|
|
86
|
+
? 'Anonymous'
|
|
87
|
+
: variant === 'externalPrincipal'
|
|
88
|
+
? 'External principal'
|
|
89
|
+
: 'External resource';
|
|
90
|
+
throw new Error(`${variantName} simulations do not support wildcard-only actions.`);
|
|
91
|
+
}
|
|
92
|
+
if (variant === 'anonymous' && !simulationRequest.resourceArn) {
|
|
93
|
+
throw new Error('Anonymous simulations require a concrete resource ARN.');
|
|
94
|
+
}
|
|
95
|
+
if (variant === 'externalResource' && !simulationRequest.resourceArn) {
|
|
96
|
+
throw new Error('External resource simulations require a concrete resource ARN.');
|
|
97
|
+
}
|
|
32
98
|
if (actionDetails.isWildcardOnly) {
|
|
33
99
|
simulationRequest.resourceAccount = (0, iam_utils_1.splitArnParts)(simulationRequest.principal).accountId;
|
|
34
100
|
}
|
|
@@ -41,165 +107,255 @@ async function simulateRequest(simulationRequest, collectClient) {
|
|
|
41
107
|
if (!simulationRequest.resourceAccount) {
|
|
42
108
|
throw new Error(`Unable to find account ID for resource ${simulationRequest.resourceArn}`);
|
|
43
109
|
}
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
110
|
+
const requiresCollectedPrincipal = variant === 'normal' || variant === 'externalResource';
|
|
111
|
+
if (requiresCollectedPrincipal) {
|
|
112
|
+
const principalFound = await (0, principals_js_1.principalExists)(simulationRequest.principal, collectClient);
|
|
113
|
+
if (!principalFound) {
|
|
114
|
+
throw new Error(`Principal ${simulationRequest.principal} does not exist. Use --external-principal to test principals outside your dataset.`);
|
|
115
|
+
}
|
|
47
116
|
}
|
|
117
|
+
const principalPolicyInputs = await principalPoliciesForVariant(variant, simulationRequest, collectClient);
|
|
118
|
+
const resourcePolicyInputs = await resourcePoliciesForVariant(variant, simulationRequest, service, actionDetails.isWildcardOnly, principalPolicyInputs.principalAccountRcps, collectClient);
|
|
48
119
|
const request = {
|
|
49
120
|
action: simulationRequest.action,
|
|
50
121
|
resource: {
|
|
51
122
|
resource: simulationRequest.resourceArn || '*',
|
|
52
123
|
accountId: simulationRequest.resourceAccount
|
|
53
124
|
},
|
|
54
|
-
principal: simulationRequest.principal,
|
|
125
|
+
principal: variant === 'anonymous' ? iam_simulate_1.anonymousPrincipal : simulationRequest.principal,
|
|
55
126
|
contextVariables: {}
|
|
56
127
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new Error(`Trust policy not found for resource ${simulationRequest.resourceArn}. sts assume role actions require a trust policy.`);
|
|
64
|
-
}
|
|
65
|
-
const principalArnForContext = await resolvePrincipalArnForContext(collectClient, simulationRequest.principal);
|
|
66
|
-
const { contextKeys, resourceTagsAreKnown } = await (0, contextKeys_js_1.createContextKeys)(collectClient, simulationRequest, service, simulationRequest.customContextKeys, principalArnForContext);
|
|
67
|
-
const vpcEndpointId = (0, contextKeys_js_1.contextValue)(contextKeys, contextKeys_js_1.CONTEXT_KEYS.vpcEndpointId);
|
|
68
|
-
let vpcEndpointPolicy = undefined;
|
|
69
|
-
if (vpcEndpointId && typeof vpcEndpointId === 'string') {
|
|
70
|
-
const vpcEndpointArn = await collectClient.getVpcEndpointArnForVpcEndpointId(vpcEndpointId);
|
|
71
|
-
if (vpcEndpointArn) {
|
|
72
|
-
const vpcPolicy = await collectClient.getVpcEndpointPolicyForArn(vpcEndpointArn);
|
|
73
|
-
if (vpcPolicy) {
|
|
74
|
-
vpcEndpointPolicy = { name: vpcEndpointArn, policy: vpcPolicy };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const applicableScps = (0, principals_js_1.isServiceLinkedRole)(simulationRequest.principal)
|
|
79
|
-
? []
|
|
80
|
-
: principalPolicies.scps;
|
|
128
|
+
const principalArnForContext = simulationRequest.principal
|
|
129
|
+
? await resolvePrincipalArnForContext(collectClient, simulationRequest.principal)
|
|
130
|
+
: undefined;
|
|
131
|
+
const includeResourceMetadata = variant !== 'externalResource';
|
|
132
|
+
const { contextKeys, resourceTagsAreKnown } = await (0, contextKeys_js_1.createContextKeys)(collectClient, simulationRequest, service, simulationRequest.customContextKeys, principalArnForContext, { includeResourceMetadata });
|
|
133
|
+
const vpcEndpointPolicy = await getVpcEndpointPolicy(collectClient, contextKeys);
|
|
81
134
|
request.contextVariables = contextKeys;
|
|
82
135
|
const simulation = {
|
|
83
136
|
request,
|
|
84
|
-
sessionPolicy: simulationRequest.sessionPolicy,
|
|
85
|
-
identityPolicies:
|
|
86
|
-
serviceControlPolicies:
|
|
87
|
-
resourceControlPolicies:
|
|
88
|
-
resourcePolicy:
|
|
89
|
-
permissionBoundaryPolicies:
|
|
137
|
+
sessionPolicy: variant === 'anonymous' ? undefined : simulationRequest.sessionPolicy,
|
|
138
|
+
identityPolicies: principalPolicyInputs.identityPolicies,
|
|
139
|
+
serviceControlPolicies: principalPolicyInputs.serviceControlPolicies,
|
|
140
|
+
resourceControlPolicies: resourcePolicyInputs.resourceControlPolicies,
|
|
141
|
+
resourcePolicy: resourcePolicyInputs.resourcePolicy,
|
|
142
|
+
permissionBoundaryPolicies: principalPolicyInputs.permissionBoundaryPolicies,
|
|
90
143
|
vpcEndpointPolicies: vpcEndpointPolicy ? [vpcEndpointPolicy] : undefined
|
|
91
144
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
await applyS3Settings(simulation, simulationRequest, variant, collectClient);
|
|
146
|
+
const strictContextKeys = makeStrictContextKeys(simulationRequest, contextKeys, resourceTagsAreKnown, (0, iam_utils_1.isS3BucketOrObjectArn)(simulationRequest.resourceArn || ''));
|
|
147
|
+
const discoveryContextKeyConstraints = strictContextKeys.map((keyName) => discoveryConstraintForStrictKey(keyName, simulationRequest));
|
|
148
|
+
const result = await (0, iam_simulate_1.runSimulation)(simulation, {
|
|
149
|
+
simulationMode: simulationRequest.simulationMode,
|
|
150
|
+
discoveryContextKeyConstraints
|
|
151
|
+
});
|
|
152
|
+
return { request, result };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build all principal-side policy inputs for the selected simulation variant.
|
|
156
|
+
*
|
|
157
|
+
* Collected-principal variants prepare collected identity policies, SCPs, and permission
|
|
158
|
+
* boundaries. External principals receive only a synthetic administrator identity policy, while
|
|
159
|
+
* anonymous requests receive no principal-side policies.
|
|
160
|
+
*
|
|
161
|
+
* @param variant the simulation policy-source variant
|
|
162
|
+
* @param request the normalized request
|
|
163
|
+
* @param collectClient the data client
|
|
164
|
+
* @returns fully prepared principal-side simulation inputs
|
|
165
|
+
*/
|
|
166
|
+
async function principalPoliciesForVariant(variant, request, collectClient) {
|
|
167
|
+
if (variant === 'anonymous') {
|
|
168
|
+
return emptyPrincipalPolicyInputs();
|
|
169
|
+
}
|
|
170
|
+
if (variant === 'externalPrincipal') {
|
|
171
|
+
return {
|
|
172
|
+
...emptyPrincipalPolicyInputs(),
|
|
173
|
+
identityPolicies: [generatedPolicies_js_1.syntheticIdentityPolicy]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const principalArn = request.principal;
|
|
177
|
+
const collectedPolicies = await (0, principals_js_1.getAllPoliciesForPrincipal)(collectClient, principalArn);
|
|
178
|
+
return {
|
|
179
|
+
identityPolicies: prepareIdentityPolicies(principalArn, collectedPolicies),
|
|
180
|
+
serviceControlPolicies: collectedPolicies.scps,
|
|
181
|
+
permissionBoundaryPolicies: preparePermissionBoundary(collectedPolicies),
|
|
182
|
+
principalAccountRcps: collectedPolicies.rcps
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Return principal-side policy inputs with no applicable policies.
|
|
187
|
+
*
|
|
188
|
+
* @returns empty principal-side simulation inputs
|
|
189
|
+
*/
|
|
190
|
+
function emptyPrincipalPolicyInputs() {
|
|
191
|
+
return {
|
|
192
|
+
identityPolicies: [],
|
|
193
|
+
serviceControlPolicies: [],
|
|
194
|
+
permissionBoundaryPolicies: undefined,
|
|
195
|
+
principalAccountRcps: []
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build all resource-side policy inputs for the selected simulation variant.
|
|
200
|
+
*
|
|
201
|
+
* This selects external versus collected resource policy behavior. All collected resource-policy
|
|
202
|
+
* and RCP applicability is delegated to `getResourcePolicies`.
|
|
203
|
+
*
|
|
204
|
+
* @param variant the simulation policy-source variant
|
|
205
|
+
* @param request the normalized request
|
|
206
|
+
* @param service the requested IAM service prefix
|
|
207
|
+
* @param actionIsWildcard whether the action uses the account-wide wildcard resource
|
|
208
|
+
* @param principalAccountRcps RCP hierarchy used by collected wildcard-only requests
|
|
209
|
+
* @param collectClient the data client
|
|
210
|
+
* @returns fully prepared resource-side simulation inputs
|
|
211
|
+
*/
|
|
212
|
+
async function resourcePoliciesForVariant(variant, request, service, actionIsWildcard, principalAccountRcps, collectClient) {
|
|
213
|
+
if (variant === 'externalResource') {
|
|
214
|
+
return {
|
|
215
|
+
resourcePolicy: (0, generatedPolicies_js_1.externalResourcePolicy)(request),
|
|
216
|
+
resourceControlPolicies: []
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return getResourcePolicies(collectClient, request.action, service, request.resourceArn, request.resourceAccount, request.principal, actionIsWildcard, principalAccountRcps);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Look up the VPC endpoint policy selected by generated or caller-provided context.
|
|
223
|
+
*
|
|
224
|
+
* @param collectClient the data client
|
|
225
|
+
* @param contextKeys the normalized request context
|
|
226
|
+
* @returns the named endpoint policy when one is available
|
|
227
|
+
*/
|
|
228
|
+
async function getVpcEndpointPolicy(collectClient, contextKeys) {
|
|
229
|
+
const vpcEndpointId = (0, contextKeys_js_1.contextValue)(contextKeys, contextKeys_js_1.CONTEXT_KEYS.vpcEndpointId);
|
|
230
|
+
if (!vpcEndpointId || typeof vpcEndpointId !== 'string') {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const vpcEndpointArn = await collectClient.getVpcEndpointArnForVpcEndpointId(vpcEndpointId);
|
|
234
|
+
if (!vpcEndpointArn) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const policy = await collectClient.getVpcEndpointPolicyForArn(vpcEndpointArn);
|
|
238
|
+
return policy ? { name: vpcEndpointArn, policy } : undefined;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Apply S3 ABAC and Block Public Access settings appropriate to the simulation variant.
|
|
242
|
+
*
|
|
243
|
+
* @param simulation the iam-simulate input to update
|
|
244
|
+
* @param request the normalized request
|
|
245
|
+
* @param variant the simulation policy-source variant
|
|
246
|
+
* @param collectClient the data client
|
|
247
|
+
*/
|
|
248
|
+
async function applyS3Settings(simulation, request, variant, collectClient) {
|
|
249
|
+
if (!request.resourceArn || !(0, iam_utils_1.isS3BucketOrObjectArn)(request.resourceArn)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (variant === 'externalResource') {
|
|
98
253
|
simulation.additionalSettings = {
|
|
99
254
|
s3: {
|
|
100
|
-
bucketAbacEnabled,
|
|
101
|
-
blockPublicAccess
|
|
255
|
+
bucketAbacEnabled: request.s3AbacOverride === 'enabled',
|
|
256
|
+
blockPublicAccess: false
|
|
102
257
|
}
|
|
103
258
|
};
|
|
259
|
+
return;
|
|
104
260
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
261
|
+
const [bucketAbacEnabled, blockPublicAccess] = await Promise.all([
|
|
262
|
+
evaluateAbacForBucket(request.s3AbacOverride, collectClient, request.resourceAccount, request.resourceArn),
|
|
263
|
+
collectClient.getBlockPublicAccessEnabledForBucket(request.resourceAccount, request.resourceArn)
|
|
264
|
+
]);
|
|
265
|
+
simulation.additionalSettings = { s3: { bucketAbacEnabled, blockPublicAccess } };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Assemble strict Discovery context-key constraints for a normalized request.
|
|
269
|
+
*
|
|
270
|
+
* @param request the normalized request
|
|
271
|
+
* @param contextKeys generated and caller-provided context values
|
|
272
|
+
* @param resourceTagsAreKnown whether all resource tags are known
|
|
273
|
+
* @param s3BucketOrObjectRequest whether S3 bucket tag keys apply
|
|
274
|
+
* @returns strict literal keys and key patterns
|
|
275
|
+
*/
|
|
276
|
+
function makeStrictContextKeys(request, contextKeys, resourceTagsAreKnown, s3BucketOrObjectRequest) {
|
|
277
|
+
const strictContextKeys = [...contextKeys_js_1.knownContextKeys, ...(request.additionalStrictContextKeys ?? [])];
|
|
278
|
+
if (!request.principal) {
|
|
279
|
+
strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.userId, contextKeys_js_1.CONTEXT_KEYS.assumedRoot, '/^aws:PrincipalTag\/.*/');
|
|
117
280
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
281
|
+
else {
|
|
282
|
+
if (!(0, iam_utils_1.isIamRoleArn)(request.principal)) {
|
|
283
|
+
strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.userId);
|
|
284
|
+
}
|
|
285
|
+
if (!request.principal.endsWith(':root')) {
|
|
286
|
+
strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.assumedRoot);
|
|
287
|
+
}
|
|
122
288
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
strictContextKeys.push(key);
|
|
289
|
+
if (request.action.startsWith('s3:')) {
|
|
290
|
+
strictContextKeys.push('s3:DataAccessPointAccount', 's3:DataAccessPointArn');
|
|
126
291
|
}
|
|
127
|
-
|
|
292
|
+
strictContextKeys.push(...Object.keys(request.customContextKeys));
|
|
128
293
|
if (resourceTagsAreKnown) {
|
|
129
294
|
strictContextKeys.push('/^aws:ResourceTag\/.*/');
|
|
130
295
|
if (s3BucketOrObjectRequest) {
|
|
131
296
|
strictContextKeys.push('/^s3:BucketTag\/.*/');
|
|
132
297
|
}
|
|
133
298
|
}
|
|
134
|
-
// There also may be other tag context keys, so add those too
|
|
135
299
|
for (const key of Object.keys(contextKeys)) {
|
|
136
300
|
if (key.toLowerCase().includes('tag/')) {
|
|
137
301
|
strictContextKeys.push(key);
|
|
138
302
|
}
|
|
139
303
|
}
|
|
140
|
-
|
|
141
|
-
const result = await (0, iam_simulate_1.runSimulation)(simulation, {
|
|
142
|
-
simulationMode: simulationRequest.simulationMode,
|
|
143
|
-
discoveryContextKeyConstraints
|
|
144
|
-
});
|
|
145
|
-
return { request, result };
|
|
304
|
+
return strictContextKeys;
|
|
146
305
|
}
|
|
147
306
|
const awsSourceKeyPrefix = 'aws:source';
|
|
307
|
+
const anonymousAbsentContextKeys = new Set([
|
|
308
|
+
'aws:principalarn',
|
|
309
|
+
'aws:principalaccount',
|
|
310
|
+
'aws:principalorgid',
|
|
311
|
+
'aws:principalorgpaths',
|
|
312
|
+
'aws:username',
|
|
313
|
+
'aws:userid',
|
|
314
|
+
'aws:principalisawsservice',
|
|
315
|
+
'aws:principalservicename',
|
|
316
|
+
'aws:sourceaccount',
|
|
317
|
+
'aws:sourceorgid',
|
|
318
|
+
'aws:sourceorgpaths',
|
|
319
|
+
'aws:sourceowner',
|
|
320
|
+
'aws:sourcearn',
|
|
321
|
+
'aws:assumedroot',
|
|
322
|
+
'kms:calleraccount',
|
|
323
|
+
'/^aws:principaltag\/.*/'
|
|
324
|
+
]);
|
|
148
325
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* Caller-provided keys are always authoritative. Other strict keys preserve the
|
|
152
|
-
* existing behavior where both presence and value are authoritative. Service
|
|
153
|
-
* source context keys are different during who-can discovery: iam-lens supplies
|
|
154
|
-
* resource-derived placeholder values, but the actual source account/org for a
|
|
155
|
-
* service principal request may differ. Treating those values as unknown lets
|
|
156
|
-
* who-can return conditional access instead of incorrectly denying service-principal
|
|
157
|
-
* statements.
|
|
326
|
+
* Convert an iam-lens strict context key into an iam-simulate Discovery constraint.
|
|
158
327
|
*
|
|
159
328
|
* @param keyName the literal context key or slash-delimited key pattern
|
|
160
|
-
* @param simulationRequest the request whose context
|
|
329
|
+
* @param simulationRequest the normalized request whose context certainty is modeled
|
|
161
330
|
* @returns the Discovery constraint to pass to iam-simulate
|
|
162
331
|
* @internal
|
|
163
332
|
*/
|
|
164
333
|
function discoveryConstraintForStrictKey(keyName, simulationRequest) {
|
|
165
334
|
if (hasCustomContextKey(simulationRequest.customContextKeys, keyName)) {
|
|
166
|
-
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
335
|
+
return { keyName, presenceIsKnown: true, valueIsKnown: true };
|
|
336
|
+
}
|
|
337
|
+
if (simulationRequest.principal === undefined &&
|
|
338
|
+
anonymousAbsentContextKeys.has(keyName.toLowerCase())) {
|
|
339
|
+
return { keyName, presenceIsKnown: true, valueIsKnown: true };
|
|
171
340
|
}
|
|
172
341
|
const isServiceCallerAccountInDiscovery = keyName.localeCompare('kms:CallerAccount', undefined, { sensitivity: 'base' }) === 0 &&
|
|
173
342
|
simulationRequest.simulationMode === 'Discovery' &&
|
|
343
|
+
simulationRequest.principal !== undefined &&
|
|
174
344
|
(0, principals_js_1.isServicePrincipal)(simulationRequest.principal);
|
|
175
345
|
if (isServiceCallerAccountInDiscovery) {
|
|
176
|
-
|
|
177
|
-
// its value. Keep its presence authoritative while reporting conditional access.
|
|
178
|
-
return {
|
|
179
|
-
keyName,
|
|
180
|
-
presenceIsKnown: true,
|
|
181
|
-
valueIsKnown: false
|
|
182
|
-
};
|
|
346
|
+
return { keyName, presenceIsKnown: true, valueIsKnown: false };
|
|
183
347
|
}
|
|
184
348
|
if (keyName.slice(0, 10).localeCompare(awsSourceKeyPrefix, undefined, { sensitivity: 'base' }) === 0) {
|
|
185
|
-
return {
|
|
186
|
-
keyName,
|
|
187
|
-
presenceIsKnown: true,
|
|
188
|
-
valueIsKnown: false
|
|
189
|
-
};
|
|
349
|
+
return { keyName, presenceIsKnown: true, valueIsKnown: false };
|
|
190
350
|
}
|
|
191
|
-
return {
|
|
192
|
-
keyName,
|
|
193
|
-
presenceIsKnown: true,
|
|
194
|
-
valueIsKnown: true
|
|
195
|
-
};
|
|
351
|
+
return { keyName, presenceIsKnown: true, valueIsKnown: true };
|
|
196
352
|
}
|
|
197
353
|
/**
|
|
198
|
-
*
|
|
354
|
+
* Check whether a caller supplied a context key using IAM's case-insensitive matching.
|
|
199
355
|
*
|
|
200
|
-
* @param customContextKeys
|
|
201
|
-
* @param keyName
|
|
202
|
-
* @returns whether the caller supplied
|
|
356
|
+
* @param customContextKeys caller-provided context values
|
|
357
|
+
* @param keyName context key to look up
|
|
358
|
+
* @returns whether the caller supplied the key
|
|
203
359
|
*/
|
|
204
360
|
function hasCustomContextKey(customContextKeys, keyName) {
|
|
205
361
|
if (Object.hasOwn(customContextKeys, keyName)) {
|
|
@@ -208,65 +364,61 @@ function hasCustomContextKey(customContextKeys, keyName) {
|
|
|
208
364
|
return Object.keys(customContextKeys).some((customKey) => customKey.toLowerCase() === keyName.toLowerCase());
|
|
209
365
|
}
|
|
210
366
|
/**
|
|
211
|
-
*
|
|
367
|
+
* Retrieve a collected resource policy and all applicable RCPs for a request.
|
|
212
368
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
369
|
+
* This applies non-variant resource-policy and RCP rules, including trust-policy requirements,
|
|
370
|
+
* IAM role identity-policy resources, wildcard-only actions, service-linked roles,
|
|
371
|
+
* `kms:RetireGrant`, AWS-managed KMS keys, and the implicit full-access RCP.
|
|
215
372
|
*
|
|
216
|
-
* @param collectClient the
|
|
373
|
+
* @param collectClient the data client
|
|
217
374
|
* @param action the action being simulated
|
|
218
|
-
* @param
|
|
219
|
-
* @param
|
|
220
|
-
* @
|
|
375
|
+
* @param service the requested IAM service prefix
|
|
376
|
+
* @param resourceArn the resource ARN
|
|
377
|
+
* @param resourceAccount the resource account
|
|
378
|
+
* @param principalArn the authenticated principal ARN, when present
|
|
379
|
+
* @param actionIsWildcard whether the action uses the account-wide wildcard resource
|
|
380
|
+
* @param principalAccountRcps RCP hierarchy for the principal account
|
|
381
|
+
* @returns the resource policy and filtered RCP hierarchies applicable to the request
|
|
221
382
|
*/
|
|
222
|
-
async function getResourcePolicies(collectClient, action, resourceArn, resourceAccount) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
: await (0, resources_js_1.getRcpsForResource)(collectClient, resourceArn, resourceAccount);
|
|
238
|
-
return { resourcePolicy, resourceRcps };
|
|
239
|
-
}
|
|
240
|
-
function rcpsForRequest(principalArn, actionIsWildcard, resourceRcps, principalRcps) {
|
|
241
|
-
if ((0, principals_js_1.isServiceLinkedRole)(principalArn)) {
|
|
242
|
-
return [];
|
|
383
|
+
async function getResourcePolicies(collectClient, action, service, resourceArn, resourceAccount, principalArn, actionIsWildcard, principalAccountRcps) {
|
|
384
|
+
const resourcePolicy = resourceArn
|
|
385
|
+
? await (0, resources_js_1.getResourcePolicyForResource)(collectClient, resourceArn, resourceAccount)
|
|
386
|
+
: undefined;
|
|
387
|
+
let resourceRcps = [];
|
|
388
|
+
if (resourceArn &&
|
|
389
|
+
action.localeCompare(kmsRetireGrantAction, undefined, { sensitivity: 'base' }) !== 0) {
|
|
390
|
+
const resourceArnParts = (0, iam_utils_1.splitArnParts)(resourceArn);
|
|
391
|
+
const isAwsManagedKmsKey = resourceArnParts.service === 'kms' &&
|
|
392
|
+
resourceArnParts.resourceType === 'key' &&
|
|
393
|
+
resourceAccount !== undefined &&
|
|
394
|
+
(await collectClient.isAwsManagedKmsKey(resourceArn, resourceAccount));
|
|
395
|
+
if (!isAwsManagedKmsKey) {
|
|
396
|
+
resourceRcps = await (0, resources_js_1.getRcpsForResource)(collectClient, resourceArn, resourceAccount);
|
|
397
|
+
}
|
|
243
398
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
399
|
+
const applicableRcps = principalArn && (0, principals_js_1.isServiceLinkedRole)(principalArn)
|
|
400
|
+
? []
|
|
401
|
+
: actionIsWildcard
|
|
402
|
+
? principalAccountRcps
|
|
403
|
+
: resourceRcps;
|
|
404
|
+
if (sts_js_1.AssumeRoleActions.has(action.toLowerCase()) && !resourcePolicy) {
|
|
405
|
+
throw new Error(`Trust policy not found for resource ${resourceArn}. sts assume role actions require a trust policy.`);
|
|
247
406
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
407
|
+
const useResourcePolicy = resourceArn !== undefined && !((0, iam_utils_1.isIamRoleArn)(resourceArn) && service.toLowerCase() === 'iam');
|
|
408
|
+
return {
|
|
409
|
+
resourcePolicy: useResourcePolicy ? resourcePolicy : undefined,
|
|
410
|
+
resourceControlPolicies: applicableRcps.map((rcp) => ({
|
|
251
411
|
orgIdentifier: rcp.orgIdentifier,
|
|
252
|
-
policies: rcp.policies.filter((policy) =>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
};
|
|
256
|
-
});
|
|
412
|
+
policies: rcp.policies.filter((policy) => !policy.name.toLowerCase().endsWith('rcpfullawsaccess'))
|
|
413
|
+
}))
|
|
414
|
+
};
|
|
257
415
|
}
|
|
258
416
|
/**
|
|
259
|
-
*
|
|
417
|
+
* Resolve the canonical role ARN used for role-backed context keys.
|
|
260
418
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* passes the cheap assumed-role ARN check, avoiding unnecessary storage lookups
|
|
265
|
-
* for users, role ARNs, root principals, and service principals.
|
|
266
|
-
*
|
|
267
|
-
* @param collectClient the IAM collect client to use for resolving role ARNs
|
|
268
|
-
* @param principalArn the simulation principal ARN or service principal
|
|
269
|
-
* @returns the canonical role ARN for assumed-role sessions, otherwise undefined
|
|
419
|
+
* @param collectClient the data client
|
|
420
|
+
* @param principalArn the request principal ARN
|
|
421
|
+
* @returns the canonical role ARN for an assumed-role session, otherwise undefined
|
|
270
422
|
*/
|
|
271
423
|
async function resolvePrincipalArnForContext(collectClient, principalArn) {
|
|
272
424
|
if (!(0, iam_utils_1.isAssumedRoleArn)(principalArn)) {
|
|
@@ -275,33 +427,30 @@ async function resolvePrincipalArnForContext(collectClient, principalArn) {
|
|
|
275
427
|
return ((await collectClient.resolvePrincipalArn(principalArn)) ??
|
|
276
428
|
(0, iam_utils_1.convertAssumedRoleArnToRoleArn)(principalArn));
|
|
277
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Flatten collected managed, inline, and group policies into iam-simulate identity inputs.
|
|
432
|
+
*
|
|
433
|
+
* @param principalArn the principal whose inline policy names are qualified
|
|
434
|
+
* @param principalPolicies collected policies for the principal
|
|
435
|
+
* @returns named identity policy inputs
|
|
436
|
+
*/
|
|
278
437
|
function prepareIdentityPolicies(principalArn, principalPolicies) {
|
|
279
|
-
//Collect unique managed policies
|
|
280
438
|
const uniqueIdentityPolicies = {};
|
|
281
439
|
principalPolicies.managedPolicies.forEach((policy) => {
|
|
282
440
|
if (!uniqueIdentityPolicies[policy.arn]) {
|
|
283
|
-
uniqueIdentityPolicies[policy.arn] = {
|
|
284
|
-
name: policy.arn,
|
|
285
|
-
policy: policy.policy
|
|
286
|
-
};
|
|
441
|
+
uniqueIdentityPolicies[policy.arn] = { name: policy.arn, policy: policy.policy };
|
|
287
442
|
}
|
|
288
443
|
});
|
|
289
444
|
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
|
|
290
445
|
groupPolicy.managedPolicies.forEach((policy) => {
|
|
291
446
|
if (!uniqueIdentityPolicies[policy.arn]) {
|
|
292
|
-
uniqueIdentityPolicies[policy.arn] = {
|
|
293
|
-
name: policy.arn,
|
|
294
|
-
policy: policy.policy
|
|
295
|
-
};
|
|
447
|
+
uniqueIdentityPolicies[policy.arn] = { name: policy.arn, policy: policy.policy };
|
|
296
448
|
}
|
|
297
449
|
});
|
|
298
450
|
});
|
|
299
451
|
const identityPolicies = Object.values(uniqueIdentityPolicies);
|
|
300
452
|
principalPolicies.inlinePolicies.forEach((policy) => {
|
|
301
|
-
identityPolicies.push({
|
|
302
|
-
name: `${principalArn}#${policy.name}`,
|
|
303
|
-
policy: policy.policy
|
|
304
|
-
});
|
|
453
|
+
identityPolicies.push({ name: `${principalArn}#${policy.name}`, policy: policy.policy });
|
|
305
454
|
});
|
|
306
455
|
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
|
|
307
456
|
groupPolicy.inlinePolicies.forEach((policy) => {
|
|
@@ -313,17 +462,30 @@ function prepareIdentityPolicies(principalArn, principalPolicies) {
|
|
|
313
462
|
});
|
|
314
463
|
return identityPolicies;
|
|
315
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Convert a collected permission boundary into iam-simulate inputs.
|
|
467
|
+
*
|
|
468
|
+
* @param principalPolicies collected policies for the principal
|
|
469
|
+
* @returns the named boundary policy or undefined
|
|
470
|
+
*/
|
|
316
471
|
function preparePermissionBoundary(principalPolicies) {
|
|
317
|
-
if (principalPolicies.permissionBoundary) {
|
|
318
|
-
return
|
|
319
|
-
{
|
|
320
|
-
name: principalPolicies.permissionBoundary.arn,
|
|
321
|
-
policy: principalPolicies.permissionBoundary.policy
|
|
322
|
-
}
|
|
323
|
-
];
|
|
472
|
+
if (!principalPolicies.permissionBoundary) {
|
|
473
|
+
return undefined;
|
|
324
474
|
}
|
|
325
|
-
return
|
|
475
|
+
return [
|
|
476
|
+
{
|
|
477
|
+
name: principalPolicies.permissionBoundary.arn,
|
|
478
|
+
policy: principalPolicies.permissionBoundary.policy
|
|
479
|
+
}
|
|
480
|
+
];
|
|
326
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Compare an actual simulation decision with an optional expected result.
|
|
484
|
+
*
|
|
485
|
+
* @param expected expected result or AnyDeny
|
|
486
|
+
* @param result actual evaluation result
|
|
487
|
+
* @returns whether the result satisfies the expectation
|
|
488
|
+
*/
|
|
327
489
|
function resultMatchesExpectation(expected, result) {
|
|
328
490
|
if (!expected) {
|
|
329
491
|
return true;
|
|
@@ -334,14 +496,13 @@ function resultMatchesExpectation(expected, result) {
|
|
|
334
496
|
return expected === result;
|
|
335
497
|
}
|
|
336
498
|
/**
|
|
337
|
-
*
|
|
338
|
-
* The evaluation can be overridden by the `s3AbacOverride` parameter.
|
|
499
|
+
* Evaluate whether S3 ABAC is enabled for a bucket or object.
|
|
339
500
|
*
|
|
340
|
-
* @param s3AbacOverride
|
|
341
|
-
* @param collectClient the
|
|
342
|
-
* @param bucketAccountId the
|
|
343
|
-
* @param bucketOrObjectArn the
|
|
344
|
-
* @returns whether ABAC
|
|
501
|
+
* @param s3AbacOverride caller override or undefined for collected detection
|
|
502
|
+
* @param collectClient the data client
|
|
503
|
+
* @param bucketAccountId the bucket account
|
|
504
|
+
* @param bucketOrObjectArn the bucket or object ARN
|
|
505
|
+
* @returns whether S3 ABAC is enabled
|
|
345
506
|
*/
|
|
346
507
|
async function evaluateAbacForBucket(s3AbacOverride, collectClient, bucketAccountId, bucketOrObjectArn) {
|
|
347
508
|
if (s3AbacOverride === 'enabled') {
|