@actsecurity/iam-lens 0.1.162 → 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,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
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
if (
|
|
44
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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:
|
|
84
|
-
serviceControlPolicies:
|
|
85
|
-
resourceControlPolicies:
|
|
86
|
-
resourcePolicy:
|
|
87
|
-
permissionBoundaryPolicies:
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
strictContextKeys.push(key);
|
|
284
|
+
if (request.action.startsWith('s3:')) {
|
|
285
|
+
strictContextKeys.push('s3:DataAccessPointAccount', 's3:DataAccessPointArn');
|
|
124
286
|
}
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
349
|
+
* Check whether a caller supplied a context key using IAM's case-insensitive matching.
|
|
197
350
|
*
|
|
198
|
-
* @param customContextKeys
|
|
199
|
-
* @param keyName
|
|
200
|
-
* @returns whether the caller supplied
|
|
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,61 @@ function hasCustomContextKey(customContextKeys, keyName) {
|
|
|
206
359
|
return Object.keys(customContextKeys).some((customKey) => customKey.toLowerCase() === keyName.toLowerCase());
|
|
207
360
|
}
|
|
208
361
|
/**
|
|
209
|
-
*
|
|
362
|
+
* Retrieve a collected resource policy and all applicable RCPs for a request.
|
|
210
363
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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
|
|
368
|
+
* @param collectClient the data client
|
|
215
369
|
* @param action the action being simulated
|
|
216
|
-
* @param
|
|
217
|
-
* @param
|
|
218
|
-
* @
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
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
|
+
let resourceRcps = [];
|
|
383
|
+
if (resourceArn &&
|
|
384
|
+
action.localeCompare(kmsRetireGrantAction, undefined, { sensitivity: 'base' }) !== 0) {
|
|
385
|
+
const resourceArnParts = splitArnParts(resourceArn);
|
|
386
|
+
const isAwsManagedKmsKey = resourceArnParts.service === 'kms' &&
|
|
387
|
+
resourceArnParts.resourceType === 'key' &&
|
|
388
|
+
resourceAccount !== undefined &&
|
|
389
|
+
(await collectClient.isAwsManagedKmsKey(resourceArn, resourceAccount));
|
|
390
|
+
if (!isAwsManagedKmsKey) {
|
|
391
|
+
resourceRcps = await getRcpsForResource(collectClient, resourceArn, resourceAccount);
|
|
392
|
+
}
|
|
241
393
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
394
|
+
const applicableRcps = principalArn && isServiceLinkedRole(principalArn)
|
|
395
|
+
? []
|
|
396
|
+
: actionIsWildcard
|
|
397
|
+
? principalAccountRcps
|
|
398
|
+
: resourceRcps;
|
|
399
|
+
if (AssumeRoleActions.has(action.toLowerCase()) && !resourcePolicy) {
|
|
400
|
+
throw new Error(`Trust policy not found for resource ${resourceArn}. sts assume role actions require a trust policy.`);
|
|
245
401
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
402
|
+
const useResourcePolicy = resourceArn !== undefined && !(isIamRoleArn(resourceArn) && service.toLowerCase() === 'iam');
|
|
403
|
+
return {
|
|
404
|
+
resourcePolicy: useResourcePolicy ? resourcePolicy : undefined,
|
|
405
|
+
resourceControlPolicies: applicableRcps.map((rcp) => ({
|
|
249
406
|
orgIdentifier: rcp.orgIdentifier,
|
|
250
|
-
policies: rcp.policies.filter((policy) =>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
};
|
|
254
|
-
});
|
|
407
|
+
policies: rcp.policies.filter((policy) => !policy.name.toLowerCase().endsWith('rcpfullawsaccess'))
|
|
408
|
+
}))
|
|
409
|
+
};
|
|
255
410
|
}
|
|
256
411
|
/**
|
|
257
|
-
*
|
|
412
|
+
* Resolve the canonical role ARN used for role-backed context keys.
|
|
258
413
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
|
414
|
+
* @param collectClient the data client
|
|
415
|
+
* @param principalArn the request principal ARN
|
|
416
|
+
* @returns the canonical role ARN for an assumed-role session, otherwise undefined
|
|
268
417
|
*/
|
|
269
418
|
async function resolvePrincipalArnForContext(collectClient, principalArn) {
|
|
270
419
|
if (!isAssumedRoleArn(principalArn)) {
|
|
@@ -273,33 +422,30 @@ async function resolvePrincipalArnForContext(collectClient, principalArn) {
|
|
|
273
422
|
return ((await collectClient.resolvePrincipalArn(principalArn)) ??
|
|
274
423
|
convertAssumedRoleArnToRoleArn(principalArn));
|
|
275
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Flatten collected managed, inline, and group policies into iam-simulate identity inputs.
|
|
427
|
+
*
|
|
428
|
+
* @param principalArn the principal whose inline policy names are qualified
|
|
429
|
+
* @param principalPolicies collected policies for the principal
|
|
430
|
+
* @returns named identity policy inputs
|
|
431
|
+
*/
|
|
276
432
|
function prepareIdentityPolicies(principalArn, principalPolicies) {
|
|
277
|
-
//Collect unique managed policies
|
|
278
433
|
const uniqueIdentityPolicies = {};
|
|
279
434
|
principalPolicies.managedPolicies.forEach((policy) => {
|
|
280
435
|
if (!uniqueIdentityPolicies[policy.arn]) {
|
|
281
|
-
uniqueIdentityPolicies[policy.arn] = {
|
|
282
|
-
name: policy.arn,
|
|
283
|
-
policy: policy.policy
|
|
284
|
-
};
|
|
436
|
+
uniqueIdentityPolicies[policy.arn] = { name: policy.arn, policy: policy.policy };
|
|
285
437
|
}
|
|
286
438
|
});
|
|
287
439
|
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
|
|
288
440
|
groupPolicy.managedPolicies.forEach((policy) => {
|
|
289
441
|
if (!uniqueIdentityPolicies[policy.arn]) {
|
|
290
|
-
uniqueIdentityPolicies[policy.arn] = {
|
|
291
|
-
name: policy.arn,
|
|
292
|
-
policy: policy.policy
|
|
293
|
-
};
|
|
442
|
+
uniqueIdentityPolicies[policy.arn] = { name: policy.arn, policy: policy.policy };
|
|
294
443
|
}
|
|
295
444
|
});
|
|
296
445
|
});
|
|
297
446
|
const identityPolicies = Object.values(uniqueIdentityPolicies);
|
|
298
447
|
principalPolicies.inlinePolicies.forEach((policy) => {
|
|
299
|
-
identityPolicies.push({
|
|
300
|
-
name: `${principalArn}#${policy.name}`,
|
|
301
|
-
policy: policy.policy
|
|
302
|
-
});
|
|
448
|
+
identityPolicies.push({ name: `${principalArn}#${policy.name}`, policy: policy.policy });
|
|
303
449
|
});
|
|
304
450
|
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
|
|
305
451
|
groupPolicy.inlinePolicies.forEach((policy) => {
|
|
@@ -311,17 +457,30 @@ function prepareIdentityPolicies(principalArn, principalPolicies) {
|
|
|
311
457
|
});
|
|
312
458
|
return identityPolicies;
|
|
313
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Convert a collected permission boundary into iam-simulate inputs.
|
|
462
|
+
*
|
|
463
|
+
* @param principalPolicies collected policies for the principal
|
|
464
|
+
* @returns the named boundary policy or undefined
|
|
465
|
+
*/
|
|
314
466
|
function preparePermissionBoundary(principalPolicies) {
|
|
315
|
-
if (principalPolicies.permissionBoundary) {
|
|
316
|
-
return
|
|
317
|
-
{
|
|
318
|
-
name: principalPolicies.permissionBoundary.arn,
|
|
319
|
-
policy: principalPolicies.permissionBoundary.policy
|
|
320
|
-
}
|
|
321
|
-
];
|
|
467
|
+
if (!principalPolicies.permissionBoundary) {
|
|
468
|
+
return undefined;
|
|
322
469
|
}
|
|
323
|
-
return
|
|
470
|
+
return [
|
|
471
|
+
{
|
|
472
|
+
name: principalPolicies.permissionBoundary.arn,
|
|
473
|
+
policy: principalPolicies.permissionBoundary.policy
|
|
474
|
+
}
|
|
475
|
+
];
|
|
324
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Compare an actual simulation decision with an optional expected result.
|
|
479
|
+
*
|
|
480
|
+
* @param expected expected result or AnyDeny
|
|
481
|
+
* @param result actual evaluation result
|
|
482
|
+
* @returns whether the result satisfies the expectation
|
|
483
|
+
*/
|
|
325
484
|
export function resultMatchesExpectation(expected, result) {
|
|
326
485
|
if (!expected) {
|
|
327
486
|
return true;
|
|
@@ -332,14 +491,13 @@ export function resultMatchesExpectation(expected, result) {
|
|
|
332
491
|
return expected === result;
|
|
333
492
|
}
|
|
334
493
|
/**
|
|
335
|
-
*
|
|
336
|
-
* The evaluation can be overridden by the `s3AbacOverride` parameter.
|
|
494
|
+
* Evaluate whether S3 ABAC is enabled for a bucket or object.
|
|
337
495
|
*
|
|
338
|
-
* @param s3AbacOverride
|
|
339
|
-
* @param collectClient the
|
|
340
|
-
* @param bucketAccountId the
|
|
341
|
-
* @param bucketOrObjectArn the
|
|
342
|
-
* @returns whether ABAC
|
|
496
|
+
* @param s3AbacOverride caller override or undefined for collected detection
|
|
497
|
+
* @param collectClient the data client
|
|
498
|
+
* @param bucketAccountId the bucket account
|
|
499
|
+
* @param bucketOrObjectArn the bucket or object ARN
|
|
500
|
+
* @returns whether S3 ABAC is enabled
|
|
343
501
|
*/
|
|
344
502
|
async function evaluateAbacForBucket(s3AbacOverride, collectClient, bucketAccountId, bucketOrObjectArn) {
|
|
345
503
|
if (s3AbacOverride === 'enabled') {
|