@cloud-copilot/iam-lens
Version:
Visibility in IAM in and across AWS accounts
241 lines • 11.1 kB
JavaScript
import { iamActionDetails, iamActionExists, iamServiceExists } from '@cloud-copilot/iam-data';
import {} from '@cloud-copilot/iam-policy';
import { runSimulation } from '@cloud-copilot/iam-simulate';
import { isIamRoleArn, isS3BucketOrObjectArn, splitArnParts } from '@cloud-copilot/iam-utils';
import { IamCollectClient } from '../collect/client.js';
import { getAllPoliciesForPrincipal, isServiceLinkedRole, principalExists } from '../principals.js';
import { getAccountIdForResource, getRcpsForResource, getResourcePolicyForResource } from '../resources.js';
import {} from '../utils/s3Abac.js';
import { AssumeRoleActions } from '../utils/sts.js';
import { CONTEXT_KEYS, contextValue, createContextKeys, knownContextKeys } from './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.
*/
export async function simulateRequest(simulationRequest, collectClient) {
const actionParts = simulationRequest.action.split(':');
const service = actionParts[0];
const serviceAction = actionParts[1];
const serviceExists = await iamServiceExists(service);
const actionExists = serviceExists && (await iamActionExists(service, serviceAction));
if (!serviceExists || !actionExists) {
throw new Error(`Unable to find action details for ${simulationRequest.action}`);
}
const actionDetails = await iamActionDetails(service, serviceAction);
// If it is a wildcard action, the resource account is always the principal account
if (actionDetails.isWildcardOnly) {
simulationRequest.resourceAccount = 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 getAccountIdForResource(collectClient, simulationRequest.resourceArn));
if (!simulationRequest.resourceAccount) {
throw new Error(`Unable to find account ID for resource ${simulationRequest.resourceArn}`);
}
const principalFound = await 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 getAllPoliciesForPrincipal(collectClient, simulationRequest.principal);
const { resourcePolicy, resourceRcps } = await getResourcePolicies(collectClient, simulationRequest.resourceArn, simulationRequest.resourceAccount);
const useResourcePolicy = simulationRequest.resourceArn &&
!(isIamRoleArn(simulationRequest.resourceArn) && service.toLowerCase() === 'iam');
if (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 createContextKeys(collectClient, simulationRequest, service, simulationRequest.customContextKeys);
const vpcEndpointId = contextValue(contextKeys, 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 = 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 && 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 = [
...knownContextKeys,
...(simulationRequest.additionalStrictContextKeys ?? [])
];
if (!isIamRoleArn(simulationRequest.principal)) {
strictContextKeys.push(CONTEXT_KEYS.userId);
}
if (!simulationRequest.principal.endsWith(':root')) {
// Treat this as strict unless it is a root principal
strictContextKeys.push(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 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 getResourcePolicyForResource(collectClient, resourceArn, resourceAccount);
const resourceRcps = await getRcpsForResource(collectClient, resourceArn, resourceAccount);
return { resourcePolicy, resourceRcps };
}
function rcpsForRequest(principalArn, actionIsWildcard, resourceRcps, principalRcps) {
if (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;
}
export 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