UNPKG

@cloud-copilot/iam-lens

Version:

Visibility in IAM in and across AWS accounts

357 lines 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.whoCan = whoCan; exports.uniqueAccountsToCheck = uniqueAccountsToCheck; exports.accountsToCheckBasedOnResourcePolicy = accountsToCheckBasedOnResourcePolicy; exports.actionsForWhoCan = actionsForWhoCan; exports.lookupActionsForResourceArn = lookupActionsForResourceArn; exports.findResourceTypeForArn = findResourceTypeForArn; exports.convertResourcePatternToRegex = convertResourcePatternToRegex; const iam_data_1 = require("@cloud-copilot/iam-data"); const iam_policy_1 = require("@cloud-copilot/iam-policy"); const iam_utils_1 = require("@cloud-copilot/iam-utils"); const resources_js_1 = require("../resources.js"); const simulate_js_1 = require("../simulate/simulate.js"); const arn_js_1 = require("../utils/arn.js"); const sts_js_1 = require("../utils/sts.js"); async function whoCan(collectClient, request) { const { resource } = request; if (!request.resourceAccount && !request.resource) { throw new Error('Either resourceAccount or resource must be provided in the request.'); } if (resource && !resource.startsWith('arn:')) { throw new Error(`Invalid resource ARN: ${resource}. It must start with 'arn:'.`); } const resourceAccount = request.resourceAccount || (await (0, resources_js_1.getAccountIdForResource)(collectClient, resource)); if (!resourceAccount) { throw new Error(`Could not determine account ID for resource ${resource}`); } const actions = await actionsForWhoCan(request); if (!actions || actions.length === 0) { throw new Error('No valid actions provided or found for the resource.'); } let resourcePolicy = undefined; if (resource) { resourcePolicy = await (0, resources_js_1.getResourcePolicyForResource)(collectClient, resource, resourceAccount); const resourceArn = new arn_js_1.Arn(resource); if ((resourceArn.matches({ service: 'iam', resourceType: 'role' }) || resourceArn.matches({ service: 'kms', resourceType: 'key' })) && !resourcePolicy) { throw new Error(`Unable to find resource policy for ${resource}. Cannot determine who can access the resource.`); } } const accountsToCheck = await accountsToCheckBasedOnResourcePolicy(resourcePolicy, resourceAccount); const uniqueAccounts = await uniqueAccountsToCheck(collectClient, accountsToCheck); const whoCanResults = []; for (const account of uniqueAccounts.accounts) { const principals = await collectClient.getAllPrincipalsInAccount(account); for (const principal of principals) { const principalResults = await runPrincipalForActions(collectClient, principal, resource, resourceAccount, actions); whoCanResults.push(...principalResults); } } const principalsNotFound = []; for (const principal of accountsToCheck.specificPrincipals) { if ((0, iam_utils_1.isServicePrincipal)(principal)) { const principalResults = await runPrincipalForActions(collectClient, principal, resource, resourceAccount, actions); whoCanResults.push(...principalResults); } else if ((0, iam_utils_1.isIamUserArn)(principal) || (0, iam_utils_1.isIamRoleArn)(principal) || (0, iam_utils_1.isAssumedRoleArn)(principal)) { const principalExists = await collectClient.principalExists(principal); if (!principalExists) { principalsNotFound.push(principal); } else { const principalResults = await runPrincipalForActions(collectClient, principal, resource, resourceAccount, actions); whoCanResults.push(...principalResults); } } else { principalsNotFound.push(principal); } } return { allowed: whoCanResults, allAccountsChecked: accountsToCheck.allAccounts, accountsNotFound: uniqueAccounts.accountsNotFound, organizationsNotFound: uniqueAccounts.organizationsNotFound, organizationalUnitsNotFound: uniqueAccounts.organizationalUnitsNotFound, principalsNotFound: principalsNotFound }; } async function runPrincipalForActions(collectClient, principal, resource, resourceAccount, actions) { const results = []; for (const action of actions) { const [service, serviceAction] = action.split(':'); const discoveryResult = await (0, simulate_js_1.simulateRequest)({ principal: principal, resourceArn: resource, resourceAccount, action, customContextKeys: {}, simulationMode: 'Discovery' }, collectClient); if (discoveryResult?.result.analysis?.result === 'Allowed') { const result = await (0, simulate_js_1.simulateRequest)({ principal: principal, resourceArn: resource, resourceAccount, action, customContextKeys: {}, simulationMode: 'Strict' }, collectClient); if (result?.result.analysis?.result === 'Allowed') { const actionType = await getActionLevel(service, serviceAction); results.push({ principal, service: service, action: serviceAction, level: actionType.toLowerCase() }); } else { const actionType = await getActionLevel(service, serviceAction); results.push({ principal, service: service, action: serviceAction, level: actionType.toLowerCase(), conditions: discoveryResult?.result.analysis.ignoredConditions, dependsOnSessionName: discoveryResult?.result.analysis.ignoredRoleSessionName ? true : undefined }); } } } return results; } /** * Get the action level for a specific service action, will fail if the service or action does not exist. * * @param service the service the action belongs to * @param action the action to get the level for * @returns the access level of the action, e.g. 'Read', 'Write', 'List', 'Tagging', 'Permissions management', 'Other' */ async function getActionLevel(service, action) { const details = await (0, iam_data_1.iamActionDetails)(service, action); return details.accessLevel; } async function uniqueAccountsToCheck(collectClient, accountsToCheck) { const returnValue = { accountsNotFound: [], organizationsNotFound: [], organizationalUnitsNotFound: [], accounts: [] }; if (accountsToCheck.allAccounts) { returnValue.accounts = await collectClient.allAccounts(); return returnValue; } const uniqueAccounts = new Set(); for (const account of accountsToCheck.specificAccounts || []) { const accountExists = await collectClient.accountExists(account); if (accountExists) { uniqueAccounts.add(account); } else { returnValue.accountsNotFound.push(account); } } for (const ouPath of accountsToCheck.specificOrganizationalUnits || []) { const parts = ouPath.split('/'); const orgId = parts[0]; const pathParts = parts.slice(1); const [found, accounts] = await collectClient.getAccountsForOrgPath(orgId, pathParts); for (const account of accounts) { uniqueAccounts.add(account); } if (!found) { returnValue.organizationalUnitsNotFound.push(ouPath); } } for (const orgId of accountsToCheck.specificOrganizations || []) { const [found, accounts] = await collectClient.getAccountsForOrganization(orgId); for (const account of accounts) { uniqueAccounts.add(account); } if (!found) { returnValue.organizationsNotFound.push(orgId); } } returnValue.accounts = Array.from(uniqueAccounts); return returnValue; } async function accountsToCheckBasedOnResourcePolicy(resourcePolicy, resourceAccount) { const accountsToCheck = { allAccounts: false, specificAccounts: [], specificPrincipals: [], specificOrganizations: [], specificOrganizationalUnits: [] }; if (resourceAccount) { accountsToCheck.specificAccounts.push(resourceAccount); } if (!resourcePolicy) { return accountsToCheck; } const policy = (0, iam_policy_1.loadPolicy)(resourcePolicy); for (const statement of policy.statements()) { if (statement.isAllow() && statement.isNotPrincipalStatement()) { accountsToCheck.allAccounts = true; } if (statement.isAllow() && statement.isPrincipalStatement()) { const principals = statement.principals(); let hasWildcardPrincipal = false; for (const principal of principals) { if (principal.isWildcardPrincipal()) { hasWildcardPrincipal = true; } else if (principal.isAccountPrincipal()) { accountsToCheck.specificAccounts.push(principal.accountId()); } else { accountsToCheck.specificPrincipals.push(principal.value()); } } if (hasWildcardPrincipal) { const specificOrgs = []; const specificOus = []; const specificAccounts = []; const conditions = statement.conditions(); for (const cond of conditions) { if (cond.conditionKey().toLowerCase() === 'aws:principalorgid' && cond.operation().value().toLowerCase().startsWith('stringequals') && !cond.conditionValues().some((v) => v.includes('$')) // Ignore dynamic values for now ) { specificOrgs.push(...cond.conditionValues()); } if (cond.conditionKey().toLowerCase() === 'aws:principalorgpaths' && cond.operation().baseOperator().toLowerCase().startsWith('stringequals') && !cond.conditionValues().some((v) => v.includes('$')) // Ignore dynamic values for now ) { specificOus.push(...cond.conditionValues()); } if (cond.conditionKey().toLowerCase() === 'aws:principalaccount' && cond.operation().value().toLowerCase().startsWith('stringequals') && !cond.conditionValues().some((v) => v.includes('$')) // Ignore dynamic values for now ) { specificAccounts.push(...cond.conditionValues()); } } if (specificAccounts.length > 0) { accountsToCheck.specificAccounts.push(...specificAccounts); } else if (specificOus.length > 0) { accountsToCheck.specificOrganizationalUnits.push(...specificOus); } else if (specificOrgs.length > 0) { accountsToCheck.specificOrganizations.push(...specificOrgs); } else { accountsToCheck.allAccounts = true; } } } } return accountsToCheck; } async function actionsForWhoCan(request) { const { actions } = request; if (actions && actions.length > 0) { const validActions = []; for (const action of actions) { const parts = action.split(':'); if (parts.length !== 2) { continue; } const [service, actionName] = parts; const serviceExists = await (0, iam_data_1.iamServiceExists)(service); if (!serviceExists) { continue; } const actionExists = await (0, iam_data_1.iamActionExists)(service, actionName); if (!actionExists) { continue; } validActions.push(action); } return validActions; } if (!request.resource) { return []; } return lookupActionsForResourceArn(request.resource); } /** * Get the the possible resource types for an action and resource * * @param service the service the action belongs to * @param action the action to get the resource type for * @param resourceArn the resource type matching the action, if any * @throws an error if the service or action does not exist, or if the action is a wildcard only action */ async function lookupActionsForResourceArn(resourceArn) { const [service, resourceType] = await findResourceTypeForArn(resourceArn); const resourceTypeKey = resourceType.key; const selectedActions = []; const serviceActions = await (0, iam_data_1.iamActionsForService)(service); for (const action of serviceActions) { const actionDetails = await (0, iam_data_1.iamActionDetails)(service, action); for (const rt of actionDetails.resourceTypes) { if (rt.name == resourceTypeKey) { selectedActions.push(`${service}:${action}`); break; // No need to check other resource types for this action } } } const isRole = new arn_js_1.Arn(resourceArn).matches({ service: 'iam', resourceType: 'role' }); if (isRole) { selectedActions.push(...sts_js_1.AssumeRoleActions.values()); } return selectedActions; } async function findResourceTypeForArn(resourceArn) { const arnParts = (0, iam_utils_1.splitArnParts)(resourceArn); const service = arnParts.service.toLowerCase(); const serviceExists = await (0, iam_data_1.iamServiceExists)(service); if (!serviceExists) { throw new Error(`Unable to find service ${service} for resource ${resourceArn}`); } const sortedResourceTypes = await allResourceTypesByArnLength(service); for (const rt of sortedResourceTypes) { const pattern = convertResourcePatternToRegex(rt.arn); const match = resourceArn.match(new RegExp(pattern)); if (match) { return [service, rt]; } } throw new Error(`Unable to find resource type for service ${service} and resource ${resourceArn}.`); } /** * Convert a resource pattern from iam-data to a regex pattern * * @param pattern the pattern to convert to a regex * @returns the regex pattern */ function convertResourcePatternToRegex(pattern) { const regex = pattern.replace(/\$\{.*?\}/g, (match, position) => { const name = match.substring(2, match.length - 1); const camelName = name.at(0)?.toLowerCase() + name.substring(1); return `(?<${camelName}>(.+?))`; }); return `^${regex}$`; } async function allResourceTypesByArnLength(service) { const resourceTypeKeys = await (0, iam_data_1.iamResourceTypesForService)(service); const sortedResourceTypes = []; for (const key of resourceTypeKeys) { const details = await (0, iam_data_1.iamResourceTypeDetails)(service, key); sortedResourceTypes.push(details); } return sortedResourceTypes.sort((a, b) => { return b.arn.length - a.arn.length; }); } //# sourceMappingURL=whoCan.js.map