@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.
Files changed (54) hide show
  1. package/README.md +30 -1
  2. package/dist/cjs/cli.js +38 -15
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/index.d.ts +1 -1
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +4 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/principals.d.ts +1 -0
  9. package/dist/cjs/principals.d.ts.map +1 -1
  10. package/dist/cjs/principals.js +5 -2
  11. package/dist/cjs/principals.js.map +1 -1
  12. package/dist/cjs/simulate/cliVariant.d.ts +116 -0
  13. package/dist/cjs/simulate/cliVariant.d.ts.map +1 -0
  14. package/dist/cjs/simulate/cliVariant.js +87 -0
  15. package/dist/cjs/simulate/cliVariant.js.map +1 -0
  16. package/dist/cjs/simulate/contextKeys.d.ts +23 -2
  17. package/dist/cjs/simulate/contextKeys.d.ts.map +1 -1
  18. package/dist/cjs/simulate/contextKeys.js +41 -11
  19. package/dist/cjs/simulate/contextKeys.js.map +1 -1
  20. package/dist/cjs/simulate/generatedPolicies.d.ts +23 -0
  21. package/dist/cjs/simulate/generatedPolicies.d.ts.map +1 -0
  22. package/dist/cjs/simulate/generatedPolicies.js +60 -0
  23. package/dist/cjs/simulate/generatedPolicies.js.map +1 -0
  24. package/dist/cjs/simulate/simulate.d.ts +107 -21
  25. package/dist/cjs/simulate/simulate.d.ts.map +1 -1
  26. package/dist/cjs/simulate/simulate.js +351 -190
  27. package/dist/cjs/simulate/simulate.js.map +1 -1
  28. package/dist/esm/cli.js +39 -16
  29. package/dist/esm/cli.js.map +1 -1
  30. package/dist/esm/index.d.ts +1 -1
  31. package/dist/esm/index.d.ts.map +1 -1
  32. package/dist/esm/index.js +1 -1
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/principals.d.ts +1 -0
  35. package/dist/esm/principals.d.ts.map +1 -1
  36. package/dist/esm/principals.js +5 -2
  37. package/dist/esm/principals.js.map +1 -1
  38. package/dist/esm/simulate/cliVariant.d.ts +116 -0
  39. package/dist/esm/simulate/cliVariant.d.ts.map +1 -0
  40. package/dist/esm/simulate/cliVariant.js +84 -0
  41. package/dist/esm/simulate/cliVariant.js.map +1 -0
  42. package/dist/esm/simulate/contextKeys.d.ts +23 -2
  43. package/dist/esm/simulate/contextKeys.d.ts.map +1 -1
  44. package/dist/esm/simulate/contextKeys.js +40 -12
  45. package/dist/esm/simulate/contextKeys.js.map +1 -1
  46. package/dist/esm/simulate/generatedPolicies.d.ts +23 -0
  47. package/dist/esm/simulate/generatedPolicies.d.ts.map +1 -0
  48. package/dist/esm/simulate/generatedPolicies.js +56 -0
  49. package/dist/esm/simulate/generatedPolicies.js.map +1 -0
  50. package/dist/esm/simulate/simulate.d.ts +107 -21
  51. package/dist/esm/simulate/simulate.d.ts.map +1 -1
  52. package/dist/esm/simulate/simulate.js +350 -192
  53. package/dist/esm/simulate/simulate.js.map +1 -1
  54. 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 against the collected IAM data.
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 simulation result, including the request and the evaluation result.
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
- const actionParts = simulationRequest.action.split(':');
23
- const service = actionParts[0];
24
- const serviceAction = actionParts[1];
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
- // If it is a wildcard action, the resource account is always the principal account
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 principalFound = await (0, principals_js_1.principalExists)(simulationRequest.principal, collectClient);
45
- if (!principalFound && !simulationRequest.ignoreMissingPrincipal) {
46
- throw new Error(`Principal ${simulationRequest.principal} does not exist. Use --ignore-missing-principal to ignore this.`);
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
- //Lookup the principal policies
58
- const principalPolicies = await (0, principals_js_1.getAllPoliciesForPrincipal)(collectClient, simulationRequest.principal);
59
- const { resourcePolicy, resourceRcps } = await getResourcePolicies(collectClient, simulationRequest.action, simulationRequest.resourceArn, simulationRequest.resourceAccount);
60
- const useResourcePolicy = simulationRequest.resourceArn &&
61
- !((0, iam_utils_1.isIamRoleArn)(simulationRequest.resourceArn) && service.toLowerCase() === 'iam');
62
- if (sts_js_1.AssumeRoleActions.has(simulationRequest.action.toLowerCase()) && !resourcePolicy) {
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: prepareIdentityPolicies(simulationRequest.principal, principalPolicies),
86
- serviceControlPolicies: applicableScps,
87
- resourceControlPolicies: rcpsForRequest(simulationRequest.principal, actionDetails.isWildcardOnly, resourceRcps, principalPolicies.rcps),
88
- resourcePolicy: useResourcePolicy ? resourcePolicy : undefined,
89
- permissionBoundaryPolicies: preparePermissionBoundary(principalPolicies),
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
- const s3BucketOrObjectRequest = simulationRequest.resourceArn && (0, iam_utils_1.isS3BucketOrObjectArn)(simulationRequest.resourceArn);
93
- if (s3BucketOrObjectRequest) {
94
- const [bucketAbacEnabled, blockPublicAccess] = await Promise.all([
95
- evaluateAbacForBucket(simulationRequest.s3AbacOverride, collectClient, simulationRequest.resourceAccount, simulationRequest.resourceArn),
96
- collectClient.getBlockPublicAccessEnabledForBucket(simulationRequest.resourceAccount, simulationRequest.resourceArn)
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
- // Assemble the strict context keys for the simulation
106
- // Start with the default known context keys
107
- const strictContextKeys = [
108
- ...contextKeys_js_1.knownContextKeys,
109
- ...(simulationRequest.additionalStrictContextKeys ?? [])
110
- ];
111
- if (!(0, iam_utils_1.isIamRoleArn)(simulationRequest.principal)) {
112
- strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.userId);
113
- }
114
- if (!simulationRequest.principal.endsWith(':root')) {
115
- // Treat this as strict unless it is a root principal
116
- strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.assumedRoot);
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
- // S3 Access Points are Not Supported Right Now, Don't Add Noise
119
- if (simulationRequest.action.startsWith('s3:')) {
120
- strictContextKeys.push('s3:DataAccessPointAccount');
121
- strictContextKeys.push('s3:DataAccessPointArn');
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
- // Add the custom context keys from the simulation request
124
- for (const key of Object.keys(simulationRequest.customContextKeys)) {
125
- strictContextKeys.push(key);
289
+ if (request.action.startsWith('s3:')) {
290
+ strictContextKeys.push('s3:DataAccessPointAccount', 's3:DataAccessPointArn');
126
291
  }
127
- //If we know the tag keys, just make all tag keys strict
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
- const discoveryContextKeyConstraints = strictContextKeys.map((keyName) => discoveryConstraintForStrictKey(keyName, simulationRequest));
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
- * Converts an iam-lens strict context key into an iam-simulate Discovery constraint.
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-key certainty is being modeled
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
- keyName,
168
- presenceIsKnown: true,
169
- valueIsKnown: true
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
- // AWS supplies this key for service-mediated KMS calls, but iam-lens does not know
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
- * Checks whether a caller supplied a context key using IAM's case-insensitive key matching.
354
+ * Check whether a caller supplied a context key using IAM's case-insensitive matching.
199
355
  *
200
- * @param customContextKeys the caller-provided context values
201
- * @param keyName the context key to look up
202
- * @returns whether the caller supplied a value for the key
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
- * Retrieves resource policies and the RCP hierarchy applicable to a simulation request.
367
+ * Retrieve a collected resource policy and all applicable RCPs for a request.
212
368
  *
213
- * AWS does not apply RCPs to `kms:RetireGrant` or AWS-managed KMS keys, so those
214
- * requests retain their resource policy while omitting the RCP hierarchy.
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 IAM collect client to use for data access
373
+ * @param collectClient the data client
217
374
  * @param action the action being simulated
218
- * @param resourceArn the ARN of the resource being accessed
219
- * @param resourceAccount the account ID of the resource, if known
220
- * @returns the resource policy and applicable RCP hierarchy for the request
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
- if (!resourceArn) {
224
- return { resourcePolicy: undefined, resourceRcps: [] };
225
- }
226
- const resourcePolicy = await (0, resources_js_1.getResourcePolicyForResource)(collectClient, resourceArn, resourceAccount);
227
- if (action.localeCompare(kmsRetireGrantAction, undefined, { sensitivity: 'base' }) === 0) {
228
- return { resourcePolicy, resourceRcps: [] };
229
- }
230
- const resourceArnParts = (0, iam_utils_1.splitArnParts)(resourceArn);
231
- const isAwsManagedKmsKey = resourceArnParts.service === 'kms' &&
232
- resourceArnParts.resourceType === 'key' &&
233
- resourceAccount !== undefined &&
234
- (await collectClient.isAwsManagedKmsKey(resourceArn, resourceAccount));
235
- const resourceRcps = isAwsManagedKmsKey
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
- let theRcps = resourceRcps;
245
- if (actionIsWildcard) {
246
- theRcps = principalRcps;
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
- return theRcps.map((rcp) => {
249
- rcp.orgIdentifier;
250
- return {
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
- return !policy.name.toLowerCase().endsWith('rcpfullawsaccess');
254
- })
255
- };
256
- });
412
+ policies: rcp.policies.filter((policy) => !policy.name.toLowerCase().endsWith('rcpfullawsaccess'))
413
+ }))
414
+ };
257
415
  }
258
416
  /**
259
- * Resolves the principal ARN to use for role-backed context keys.
417
+ * Resolve the canonical role ARN used for role-backed context keys.
260
418
  *
261
- * Direct simulations can use STS assumed-role session ARNs, but collected tags
262
- * and the canonical `aws:PrincipalArn` value belong to the underlying IAM role.
263
- * This function performs the collected-data lookup only after the principal ARN
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 undefined;
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
- * Evaluates whether ABAC (Attribute-Based Access Control) is enabled for a given S3 bucket or object.
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 the override setting for S3 ABAC or undefined to auto-detect
341
- * @param collectClient the IAM collect client to use for data access
342
- * @param bucketAccountId the account ID the bucket belongs to
343
- * @param bucketOrObjectArn the ARN of the bucket or bucket object
344
- * @returns whether ABAC should be used to evaluate access for the bucket or object
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') {