@cloud-copilot/iam-lens
Version:
Visibility in IAM in and across AWS accounts
242 lines • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.simulateRequest = simulateRequest;
exports.resultMatchesExpectation = resultMatchesExpectation;
const iam_data_1 = require("@cloud-copilot/iam-data");
const iam_simulate_1 = require("@cloud-copilot/iam-simulate");
const iam_utils_1 = require("@cloud-copilot/iam-utils");
const principals_js_1 = require("../principals.js");
const resources_js_1 = require("../resources.js");
const sts_js_1 = require("../utils/sts.js");
const contextKeys_js_1 = require("./contextKeys.js");
/**
* Simulate an IAM request against the collected IAM data.
*
* @param simulationRequest the simulation request details.
* @param collectClient the IAM collect client to use for data access.
* @returns the simulation result, including the request and the evaluation result.
*/
async function simulateRequest(simulationRequest, collectClient) {
const actionParts = simulationRequest.action.split(':');
const service = actionParts[0];
const serviceAction = actionParts[1];
const serviceExists = await (0, iam_data_1.iamServiceExists)(service);
const actionExists = serviceExists && (await (0, iam_data_1.iamActionExists)(service, serviceAction));
if (!serviceExists || !actionExists) {
throw new Error(`Unable to find action details for ${simulationRequest.action}`);
}
const actionDetails = await (0, iam_data_1.iamActionDetails)(service, serviceAction);
// If it is a wildcard action, the resource account is always the principal account
if (actionDetails.isWildcardOnly) {
simulationRequest.resourceAccount = (0, iam_utils_1.splitArnParts)(simulationRequest.principal).accountId;
}
if (!simulationRequest.resourceAccount && !simulationRequest.resourceArn) {
throw new Error('Non wildcard actions require a resource ARN or resource account to be specified.');
}
simulationRequest.resourceAccount =
simulationRequest.resourceAccount ||
(await (0, resources_js_1.getAccountIdForResource)(collectClient, simulationRequest.resourceArn));
if (!simulationRequest.resourceAccount) {
throw new Error(`Unable to find account ID for resource ${simulationRequest.resourceArn}`);
}
const principalFound = await (0, principals_js_1.principalExists)(simulationRequest.principal, collectClient);
if (!principalFound && !simulationRequest.ignoreMissingPrincipal) {
throw new Error(`Principal ${simulationRequest.principal} does not exist. Use --ignore-missing-principal to ignore this.`);
}
const request = {
action: simulationRequest.action,
resource: {
resource: simulationRequest.resourceArn || '*',
accountId: simulationRequest.resourceAccount
},
principal: simulationRequest.principal,
contextVariables: {}
};
//Lookup the principal policies
const principalPolicies = await (0, principals_js_1.getAllPoliciesForPrincipal)(collectClient, simulationRequest.principal);
const { resourcePolicy, resourceRcps } = await getResourcePolicies(collectClient, simulationRequest.resourceArn, simulationRequest.resourceAccount);
const useResourcePolicy = simulationRequest.resourceArn &&
!((0, iam_utils_1.isIamRoleArn)(simulationRequest.resourceArn) && service.toLowerCase() === 'iam');
if (sts_js_1.AssumeRoleActions.has(simulationRequest.action.toLowerCase()) && !resourcePolicy) {
throw new Error(`Trust policy not found for resource ${simulationRequest.resourceArn}. sts assume role actions require a trust policy.`);
}
const { contextKeys, resourceTagsAreKnown } = await (0, contextKeys_js_1.createContextKeys)(collectClient, simulationRequest, service, simulationRequest.customContextKeys);
const vpcEndpointId = (0, contextKeys_js_1.contextValue)(contextKeys, contextKeys_js_1.CONTEXT_KEYS.vpcEndpointId);
let vpcEndpointPolicy = undefined;
if (vpcEndpointId && typeof vpcEndpointId === 'string') {
const vpcEndpointArn = await collectClient.getVpcEndpointArnForVpcEndpointId(vpcEndpointId);
if (vpcEndpointArn) {
const vpcPolicy = await collectClient.getVpcEndpointPolicyForArn(vpcEndpointArn);
if (vpcPolicy) {
vpcEndpointPolicy = { name: vpcEndpointArn, policy: vpcPolicy };
}
}
}
const applicableScps = (0, principals_js_1.isServiceLinkedRole)(simulationRequest.principal)
? []
: principalPolicies.scps;
request.contextVariables = contextKeys;
const simulation = {
request,
sessionPolicy: simulationRequest.sessionPolicy,
identityPolicies: prepareIdentityPolicies(simulationRequest.principal, principalPolicies),
serviceControlPolicies: applicableScps,
resourceControlPolicies: rcpsForRequest(simulationRequest.principal, actionDetails.isWildcardOnly, resourceRcps, principalPolicies.rcps),
resourcePolicy: useResourcePolicy ? resourcePolicy : undefined,
permissionBoundaryPolicies: preparePermissionBoundary(principalPolicies),
vpcEndpointPolicies: vpcEndpointPolicy ? [vpcEndpointPolicy] : undefined
};
const s3BucketOrObjectRequest = simulationRequest.resourceArn && (0, iam_utils_1.isS3BucketOrObjectArn)(simulationRequest.resourceArn);
if (s3BucketOrObjectRequest) {
const bucketAbacEnabled = await evaluateAbacForBucket(simulationRequest.s3AbacOverride, collectClient, simulationRequest.resourceAccount, simulationRequest.resourceArn);
simulation.additionalSettings = {
s3: {
bucketAbacEnabled
}
};
}
// Assemble the strict context keys for the simulation
// Start with the default known context keys
const strictContextKeys = [
...contextKeys_js_1.knownContextKeys,
...(simulationRequest.additionalStrictContextKeys ?? [])
];
if (!(0, iam_utils_1.isIamRoleArn)(simulationRequest.principal)) {
strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.userId);
}
if (!simulationRequest.principal.endsWith(':root')) {
// Treat this as strict unless it is a root principal
strictContextKeys.push(contextKeys_js_1.CONTEXT_KEYS.assumedRoot);
}
// S3 Access Points are Not Supported Right Now, Don't Add Noise
if (simulationRequest.action.startsWith('s3:')) {
strictContextKeys.push('s3:DataAccessPointAccount');
strictContextKeys.push('s3:DataAccessPointArn');
}
// Add the custom context keys from the simulation request
for (const key of Object.keys(simulationRequest.customContextKeys)) {
strictContextKeys.push(key);
}
//If we know the tag keys, just make all tag keys strict
if (resourceTagsAreKnown) {
strictContextKeys.push('/^aws:ResourceTag\/.*/');
if (s3BucketOrObjectRequest) {
strictContextKeys.push('/^s3:BucketTag\/.*/');
}
}
// There also may be other tag context keys, so add those too
for (const key of Object.keys(contextKeys)) {
if (key.toLowerCase().includes('tag/')) {
strictContextKeys.push(key);
}
}
const result = await (0, iam_simulate_1.runSimulation)(simulation, {
simulationMode: simulationRequest.simulationMode,
strictConditionKeys: strictContextKeys
});
return { request, result };
}
async function getResourcePolicies(collectClient, resourceArn, resourceAccount) {
if (!resourceArn) {
return { resourcePolicy: undefined, resourceRcps: [] };
}
const resourcePolicy = await (0, resources_js_1.getResourcePolicyForResource)(collectClient, resourceArn, resourceAccount);
const resourceRcps = await (0, resources_js_1.getRcpsForResource)(collectClient, resourceArn, resourceAccount);
return { resourcePolicy, resourceRcps };
}
function rcpsForRequest(principalArn, actionIsWildcard, resourceRcps, principalRcps) {
if ((0, principals_js_1.isServiceLinkedRole)(principalArn)) {
return [];
}
let theRcps = resourceRcps;
if (actionIsWildcard) {
theRcps = principalRcps;
}
return theRcps.map((rcp) => {
rcp.orgIdentifier;
return {
orgIdentifier: rcp.orgIdentifier,
policies: rcp.policies.filter((policy) => {
return !policy.name.toLowerCase().endsWith('rcpfullawsaccess');
})
};
});
}
function prepareIdentityPolicies(principalArn, principalPolicies) {
//Collect unique managed policies
const uniqueIdentityPolicies = {};
principalPolicies.managedPolicies.forEach((policy) => {
if (!uniqueIdentityPolicies[policy.arn]) {
uniqueIdentityPolicies[policy.arn] = {
name: policy.arn,
policy: policy.policy
};
}
});
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
groupPolicy.managedPolicies.forEach((policy) => {
if (!uniqueIdentityPolicies[policy.arn]) {
uniqueIdentityPolicies[policy.arn] = {
name: policy.arn,
policy: policy.policy
};
}
});
});
const identityPolicies = Object.values(uniqueIdentityPolicies);
principalPolicies.inlinePolicies.forEach((policy) => {
identityPolicies.push({
name: `${principalArn}#${policy.name}`,
policy: policy.policy
});
});
principalPolicies.groupPolicies?.forEach((groupPolicy) => {
groupPolicy.inlinePolicies.forEach((policy) => {
identityPolicies.push({
name: `${groupPolicy.group}#${policy.name}`,
policy: policy.policy
});
});
});
return identityPolicies;
}
function preparePermissionBoundary(principalPolicies) {
if (principalPolicies.permissionBoundary) {
return [
{
name: principalPolicies.permissionBoundary.arn,
policy: principalPolicies.permissionBoundary.policy
}
];
}
return undefined;
}
function resultMatchesExpectation(expected, result) {
if (!expected) {
return true;
}
if (expected === 'AnyDeny') {
return result.includes('Denied');
}
return expected === result;
}
/**
* Evaluates whether ABAC (Attribute-Based Access Control) is enabled for a given S3 bucket or object.
* The evaluation can be overridden by the `s3AbacOverride` parameter.
*
* @param s3AbacOverride the override setting for S3 ABAC or undefined to auto-detect
* @param collectClient the IAM collect client to use for data access
* @param bucketAccountId the account ID the bucket belongs to
* @param bucketOrObjectArn the ARN of the bucket or bucket object
* @returns whether ABAC should be used to evaluate access for the bucket or object
*/
async function evaluateAbacForBucket(s3AbacOverride, collectClient, bucketAccountId, bucketOrObjectArn) {
if (s3AbacOverride === 'enabled') {
return true;
}
if (s3AbacOverride === 'disabled') {
return false;
}
return collectClient.getAbacEnabledForBucket(bucketAccountId, bucketOrObjectArn);
}
//# sourceMappingURL=simulate.js.map