@actsecurity/iam-lens 0.1.163 → 0.1.165

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