UNPKG

@cloud-copilot/iam-simulate

Version:
274 lines 10.3 kB
import { iamActionExists, iamServiceExists } from '@cloud-copilot/iam-data'; import { loadPolicy, validateEndpointPolicy, validateIdentityPolicy, validateResourceControlPolicy, validateResourcePolicy, validateServiceControlPolicy } from '@cloud-copilot/iam-policy'; import { isConditionKeyArray } from '../context_keys/contextKeyTypes.js'; import { normalizeContextKeyCase, typeForContextKey } from '../context_keys/contextKeys.js'; import { authorize, validSimulationModes } from '../core_engine/CoreSimulatorEngine.js'; import { AwsRequestImpl } from '../request/request.js'; import { RequestContextImpl } from '../requestContext.js'; import { getResourceTypesForAction, isWildcardOnlyAction } from '../util.js'; import { allowedContextKeysForRequest } from './contextKeys.js'; const DEFAULT_RCP = { name: 'RCPFullAWSAccess', policy: { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: '*', Action: '*', Resource: '*' } ] } }; /** * Run a simulation with validation * * @param simulation The simulation to run * @param simulationOptions Options for the simulation * @returns */ export async function runSimulation(simulation, simulationOptions) { const identityPolicyErrors = {}; const identityPolicies = []; simulation.identityPolicies.forEach((value) => { const { name, policy } = value; const validationErrors = validateIdentityPolicy(policy); if (validationErrors.length == 0) { identityPolicies.push(loadPolicy(policy, { name })); } else { identityPolicyErrors[name] = validationErrors; } }); const serviceControlPolicyErrors = {}; const serviceControlPolicies = simulation.serviceControlPolicies.map((scp) => { const ouId = scp.orgIdentifier; const validPolicies = []; scp.policies.forEach((value) => { const { name, policy } = value; const validationErrors = validateServiceControlPolicy(policy); if (validationErrors.length > 0) { serviceControlPolicyErrors[name] = validationErrors; } else { validPolicies.push(loadPolicy(policy, { name })); } }); return { orgIdentifier: ouId, policies: validPolicies }; }); const resourceControlPolicyErrors = {}; const resourceControlPolicies = simulation.resourceControlPolicies.map((rcp) => { const ouId = rcp.orgIdentifier; const validPolicies = []; validPolicies.push(loadPolicy(DEFAULT_RCP.policy, { name: DEFAULT_RCP.name })); rcp.policies.forEach((value) => { const { name, policy } = value; const validationErrors = validateResourceControlPolicy(policy); if (validationErrors.length > 0) { resourceControlPolicyErrors[name] = validationErrors; } else { validPolicies.push(loadPolicy(policy, { name })); } }); return { orgIdentifier: ouId, policies: validPolicies }; }); const resourcePolicyErrors = simulation.resourcePolicy ? validateResourcePolicy(simulation.resourcePolicy) : []; const permissionBoundaries = simulation.permissionBoundaryPolicies ? [] : undefined; const permissionBoundaryErrors = {}; simulation.permissionBoundaryPolicies?.map((pb) => { const { name, policy } = pb; const validationErrors = validateIdentityPolicy(policy); if (validationErrors.length == 0) { permissionBoundaries.push(loadPolicy(policy, { name })); } else { permissionBoundaryErrors[name] = validationErrors; } }); const vpcEndpointPolicies = simulation.vpcEndpointPolicies ? [] : undefined; const vpcEndpointErrors = {}; simulation.vpcEndpointPolicies?.map((endpointPolicy) => { const { name, policy } = endpointPolicy; const validationErrors = validateEndpointPolicy(policy); if (validationErrors.length == 0) { vpcEndpointPolicies.push(loadPolicy(policy, { name })); } else { vpcEndpointErrors[name] = validationErrors; } }); if (Object.keys(identityPolicyErrors).length > 0 || Object.keys(serviceControlPolicyErrors).length > 0 || Object.keys(resourceControlPolicyErrors).length > 0 || Object.keys(permissionBoundaryErrors).length > 0 || Object.keys(vpcEndpointErrors).length > 0 || resourcePolicyErrors.length > 0) { return { errors: { identityPolicyErrors, serviceControlPolicyErrors: serviceControlPolicyErrors, resourceControlPolicyErrors, resourcePolicyErrors, permissionBoundaryErrors, vpcEndpointErrors, message: 'policy.errors' } }; } const resourcePolicy = simulation.resourcePolicy ? loadPolicy(simulation.resourcePolicy, { name: simulation.resourcePolicy.name }) : undefined; if (simulation.request.action.split(':').length != 2) { return { errors: { message: 'invalid.action' } }; } const [service, action] = simulation.request.action.split(':'); const validService = await iamServiceExists(service); if (!validService) { return { errors: { message: 'invalid.service' } }; } const validAction = await iamActionExists(service, action); if (!validAction) { return { errors: { message: 'invalid.action' } }; } const resourceArn = simulation.request.resource.resource; const isWildCardOnlyAction = await isWildcardOnlyAction(service, action); let resourceType = undefined; if (isWildCardOnlyAction) { if (resourceArn !== '*') { return { errors: { message: 'must.use.wildcard' } }; } } else { const resourceTypes = await getResourceTypesForAction(service, action, resourceArn); if (resourceTypes.length === 0) { return { errors: { message: 'no.resource.types' } }; } else if (resourceTypes.length > 1) { return { errors: { message: 'multiple.resource.types' } }; } else { resourceType = resourceTypes[0].key; } } const { validContextValues, ignoredContextKeys } = await normalizeSimulationParameters(simulation); const simulationMode = validSimulationModes.includes(simulationOptions.simulationMode) ? simulationOptions.simulationMode : 'Strict'; const strictConditionKeys = simulationMode === 'Discovery' ? new Set(simulationOptions.strictConditionKeys?.map((k) => k.toLowerCase()) || []) : new Set(); const simulationResult = authorize({ request: new AwsRequestImpl(simulation.request.principal, { resource: simulation.request.resource.resource, accountId: simulation.request.resource.accountId }, simulation.request.action, new RequestContextImpl(validContextValues)), identityPolicies, serviceControlPolicies, resourceControlPolicies, resourcePolicy, permissionBoundaries, vpcEndpointPolicies, simulationParameters: { simulationMode: simulationMode, strictConditionKeys: strictConditionKeys } }); return { analysis: simulationResult, ignoredContextKeys, resourceType }; } export async function normalizeSimulationParameters(simulation) { const [service, action] = simulation.request.action.split(':'); const resourceArn = simulation.request.resource.resource; const contextVariablesForAction = new Set(await allowedContextKeysForRequest(service, action, resourceArn)); //Get the types of the context variables and set a string or array of strings based on that. const validContextValues = {}; const ignoredContextKeys = []; for (const key of Object.keys(simulation.request.contextVariables)) { const value = simulation.request.contextVariables[key]; const lowerCaseKey = key.toLowerCase(); if (contextVariablesForAction.has(lowerCaseKey) || listHasVariableKeyMatch(lowerCaseKey, contextVariablesForAction)) { const conditionType = await typeForContextKey(lowerCaseKey); const normalizedKey = await normalizeContextKeyCase(key); if (isConditionKeyArray(conditionType)) { validContextValues[normalizedKey] = [value].flat(); } else if (Array.isArray(value)) { validContextValues[normalizedKey] = value[0]; } else { validContextValues[normalizedKey] = value; } } else { ignoredContextKeys.push(key); } } return { validContextValues, ignoredContextKeys }; } /** * Evaluates a context key with a variable such as `aws:PrincipalTag/Foo` to see * if it matches any of the context variables in the allowed variables set. * * @param lowerCaseKey The lower case key to check for a match. * @param contextVariables The set of context variables to check against. * @returns True if the key has a variable match, false otherwise. */ function listHasVariableKeyMatch(lowerCaseKey, contextVariables) { const firstSlashIndex = lowerCaseKey.indexOf('/'); if (firstSlashIndex === -1) { return false; } const prefix = lowerCaseKey.slice(0, firstSlashIndex + 1); for (const variable of contextVariables) { if (variable.startsWith(prefix)) { return true; } } return false; } //# sourceMappingURL=simulationEngine.js.map