UNPKG

@cloud-copilot/iam-simulate

Version:
421 lines 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validSimulationModes = void 0; exports.authorize = authorize; exports.getServiceAuthorizer = getServiceAuthorizer; exports.analyzeIdentityPolicies = analyzeIdentityPolicies; exports.analyzeControlPolicies = analyzeControlPolicies; exports.analyzeResourcePolicy = analyzeResourcePolicy; exports.analyzePermissionBoundaryPolicies = analyzePermissionBoundaryPolicies; exports.analyzeVpcEndpointPolicies = analyzeVpcEndpointPolicies; const action_js_1 = require("../action/action.js"); const condition_js_1 = require("../condition/condition.js"); const principal_js_1 = require("../principal/principal.js"); const resource_js_1 = require("../resource/resource.js"); const DefaultServiceAuthorizer_js_1 = require("../services/DefaultServiceAuthorizer.js"); const IamServiceAuthorizer_js_1 = require("../services/IamServiceAuthorizer.js"); const KmsServiceAuthorizer_js_1 = require("../services/KmsServiceAuthorizer.js"); const StsServiceAuthorizer_js_1 = require("../services/StsServiceAuthorizer.js"); const StatementAnalysis_js_1 = require("../StatementAnalysis.js"); exports.validSimulationModes = ['Strict', 'Discovery']; const serviceEngines = { kms: KmsServiceAuthorizer_js_1.KmsServiceAuthorizer, sts: StsServiceAuthorizer_js_1.StsServiceAuthorizer, iam: IamServiceAuthorizer_js_1.IamServiceAuthorizer }; /** * Authorizes a request. * * This assumes all policies have been validated and the request is fully complete and valid. * * @param request the request to authorize * @returns the result of the authorization */ function authorize(request) { const principalHasPermissionBoundary = !!request.permissionBoundaries && request.permissionBoundaries.length > 0; const simulationParameters = request.simulationParameters; const identityAnalysis = analyzeIdentityPolicies(request.identityPolicies, request.request, simulationParameters); const permissionBoundaryAnalysis = analyzePermissionBoundaryPolicies(request.permissionBoundaries, request.request, simulationParameters); const scpAnalysis = analyzeControlPolicies(request.serviceControlPolicies, request.request, simulationParameters); const rcpAnalysis = analyzeControlPolicies(request.resourceControlPolicies, request.request, simulationParameters); const resourceAnalysis = analyzeResourcePolicy(request.resourcePolicy, request.request, principalHasPermissionBoundary, simulationParameters); const endpointAnalysis = analyzeVpcEndpointPolicies(request.vpcEndpointPolicies, request.request, simulationParameters); const serviceAuthorizer = getServiceAuthorizer(request); const result = serviceAuthorizer.authorize({ request: request.request, identityAnalysis, scpAnalysis, rcpAnalysis, resourceAnalysis, permissionBoundaryAnalysis, endpointAnalysis, simulationParameters }); if (simulationParameters.simulationMode === 'Discovery') { result.ignoredConditions = ignoredConditionsAnalysis(scpAnalysis, rcpAnalysis, identityAnalysis, resourceAnalysis, permissionBoundaryAnalysis, endpointAnalysis); result.ignoredRoleSessionName = roleSessionNameIgnored(scpAnalysis, rcpAnalysis, identityAnalysis, resourceAnalysis, permissionBoundaryAnalysis); } return result; } /** * Get the appropriate service authorizer for the request. Some services have specific authorization logic in * them. If there is no service specific authorizer, a default one will be used. * * @param request the request to get the authorizer for * @returns the service authorizer for the request */ function getServiceAuthorizer(request) { const serviceName = request.request.action.service().toLowerCase(); if (serviceEngines[serviceName]) { return new serviceEngines[serviceName](); } return new DefaultServiceAuthorizer_js_1.DefaultServiceAuthorizer(); } /** * Analyzes a set of identity policies * * @param identityPolicies the identity policies to analyze * @param request the request to analyze against * @returns an array of statement analysis results */ function analyzeIdentityPolicies(identityPolicies, request, simulationParameters) { const identityAnalysis = { result: 'ImplicitlyDenied', allowStatements: [], denyStatements: [], unmatchedStatements: [] }; for (const policy of identityPolicies) { for (const statement of policy.statements()) { const { matches: resourceMatch, details: resourceDetails } = (0, resource_js_1.requestMatchesStatementResources)(request, statement); const { matches: actionMatch, details: actionDetails } = (0, action_js_1.requestMatchesStatementActions)(request, statement); const { matches: conditionMatch, details: conditionDetails, ignoredConditions } = (0, condition_js_1.requestMatchesConditions)(request, statement.conditions(), statement.effect(), simulationParameters); const principalMatch = 'Match'; const overallMatch = (0, StatementAnalysis_js_1.statementMatches)({ actionMatch, conditionMatch, principalMatch, resourceMatch }); const shouldReportIgnoredConditions = (0, StatementAnalysis_js_1.reportIgnoredConditions)({ actionMatch, principalMatch, resourceMatch }); const statementAnalysis = { policyId: policy.metadata().name, statement, resourceMatch, actionMatch, conditionMatch, principalMatch, ignoredConditions: shouldReportIgnoredConditions ? ignoredConditions : undefined, explain: makeStatementExplain(statement, overallMatch, actionMatch, principalMatch, resourceMatch, conditionMatch, { ...resourceDetails, ...actionDetails, ...conditionDetails }) }; if ((0, StatementAnalysis_js_1.identityStatementExplicitDeny)(statementAnalysis)) { identityAnalysis.denyStatements.push(statementAnalysis); } else if ((0, StatementAnalysis_js_1.identityStatementAllows)(statementAnalysis)) { identityAnalysis.allowStatements.push(statementAnalysis); } else { identityAnalysis.unmatchedStatements.push(statementAnalysis); } } } if (identityAnalysis.denyStatements.length > 0) { identityAnalysis.result = 'ExplicitlyDenied'; } else if (identityAnalysis.allowStatements.length > 0) { identityAnalysis.result = 'Allowed'; } return identityAnalysis; } /** * Analyzes a set of service or resource control policies and the statements within them. * * @param controlPolicies the control policies to analyze * @param request the request to analyze against * @returns an array of SCP or RCP analysis results */ function analyzeControlPolicies(controlPolicies, request, simulationParameters) { const analysis = []; for (const controlPolicy of controlPolicies) { const ouAnalysis = { orgIdentifier: controlPolicy.orgIdentifier, result: 'ImplicitlyDenied', allowStatements: [], denyStatements: [], unmatchedStatements: [] }; for (const policy of controlPolicy.policies) { for (const statement of policy.statements()) { const { matches: resourceMatch, details: resourceDetails } = (0, resource_js_1.requestMatchesStatementResources)(request, statement); const { matches: actionMatch, details: actionDetails } = (0, action_js_1.requestMatchesStatementActions)(request, statement); const { matches: conditionMatch, details: conditionDetails, ignoredConditions } = (0, condition_js_1.requestMatchesConditions)(request, statement.conditions(), statement.effect(), simulationParameters); const principalMatch = 'Match'; const overallMatch = (0, StatementAnalysis_js_1.statementMatches)({ actionMatch, conditionMatch, principalMatch, resourceMatch }); const shouldReportIgnoredConditions = (0, StatementAnalysis_js_1.reportIgnoredConditions)({ actionMatch, principalMatch, resourceMatch }); const statementAnalysis = { policyId: policy.metadata().name, statement, resourceMatch, actionMatch, conditionMatch, principalMatch, ignoredConditions: shouldReportIgnoredConditions ? ignoredConditions : [], explain: makeStatementExplain(statement, overallMatch, actionMatch, principalMatch, resourceMatch, conditionMatch, { ...resourceDetails, ...actionDetails, ...conditionDetails }) }; if ((0, StatementAnalysis_js_1.identityStatementAllows)(statementAnalysis)) { ouAnalysis.allowStatements.push(statementAnalysis); } else if ((0, StatementAnalysis_js_1.identityStatementExplicitDeny)(statementAnalysis)) { ouAnalysis.denyStatements.push(statementAnalysis); } else { ouAnalysis.unmatchedStatements.push(statementAnalysis); } } } if (ouAnalysis.denyStatements.length > 0) { ouAnalysis.result = 'ExplicitlyDenied'; } else if (ouAnalysis.allowStatements.length > 0) { ouAnalysis.result = 'Allowed'; } analysis.push(ouAnalysis); } let overallResult = 'ImplicitlyDenied'; if (analysis.some((ou) => ou.result === 'ExplicitlyDenied')) { overallResult = 'ExplicitlyDenied'; } else if (analysis.some((ou) => ou.allowStatements.length === 0)) { overallResult = 'ImplicitlyDenied'; } else if (analysis.every((ou) => ou.result === 'Allowed')) { overallResult = 'Allowed'; } return { result: overallResult, ouAnalysis: analysis }; } /** * Analyze a resource policy and return the results * * @param resourcePolicy the resource policy to analyze * @param request the request to analyze against * @returns an array of statement analysis results */ function analyzeResourcePolicy(resourcePolicy, request, principalHasPermissionBoundary, simulationParameters) { const resourceAnalysis = { result: 'NotApplicable', allowStatements: [], denyStatements: [], unmatchedStatements: [] }; if (!resourcePolicy) { return resourceAnalysis; } const principalMatchOptions = [ 'Match', 'SessionRoleMatch', 'SessionUserMatch' ]; for (const statement of resourcePolicy.statements()) { const { matches: resourceMatch, details: resourceDetails } = (0, resource_js_1.requestMatchesStatementResources)(request, statement); const { matches: actionMatch, details: actionDetails } = (0, action_js_1.requestMatchesStatementActions)(request, statement); let { matches: principalMatch, details: principalDetails, ignoredRoleSessionName } = (0, principal_js_1.requestMatchesStatementPrincipals)(request, statement, simulationParameters); const permissionBoundaryDetails = {}; /** * "Don't use resource-based policy statements that include a NotPrincipal policy element with a * Deny effect for IAM users or roles that have a permissions boundary policy attached. * The NotPrincipal element with a Deny effect will always deny any IAM principal that * has a permissions boundary policy attached, regardless of the values specified in the * NotPrincipal element. This causes some IAM users or roles that would otherwise have access * to the resource to lose access. We recommend changing your resource-based policy statements * to use the condition operator ArnNotEquals with the aws:PrincipalArn context key to limit * access instead of the NotPrincipal element. For information about permissions boundaries, see * Permissions boundaries for IAM entities." * https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html */ if (principalHasPermissionBoundary && statement.isNotPrincipalStatement() && statement.effect() === 'Deny') { principalMatch = 'Match'; permissionBoundaryDetails.denyBecauseNpInRpAndPb = true; } const { matches: conditionMatch, details: conditionDetails, ignoredConditions } = (0, condition_js_1.requestMatchesConditions)(request, statement.conditions(), statement.effect(), simulationParameters); const overallMatch = (0, StatementAnalysis_js_1.statementMatches)({ actionMatch, conditionMatch, principalMatch, resourceMatch }); const shouldReportIgnoredConditions = (0, StatementAnalysis_js_1.reportIgnoredConditions)({ actionMatch, principalMatch, resourceMatch }); const analysis = { policyId: resourcePolicy.metadata().name, statement, resourceMatch: resourceMatch, actionMatch, conditionMatch, principalMatch, ignoredConditions: shouldReportIgnoredConditions ? ignoredConditions : undefined, ignoredRoleSessionName, explain: makeStatementExplain(statement, overallMatch, actionMatch, principalMatch, resourceMatch, conditionMatch, { ...resourceDetails, ...actionDetails, ...principalDetails, ...conditionDetails }) }; if ((0, StatementAnalysis_js_1.identityStatementExplicitDeny)(analysis) && analysis.principalMatch !== 'NoMatch') { resourceAnalysis.denyStatements.push(analysis); } else if ((0, StatementAnalysis_js_1.identityStatementAllows)(analysis) && analysis.principalMatch !== 'NoMatch') { resourceAnalysis.allowStatements.push(analysis); } else { resourceAnalysis.unmatchedStatements.push(analysis); } } if (resourceAnalysis.denyStatements.some((s) => principalMatchOptions.includes(s.principalMatch))) { resourceAnalysis.result = 'ExplicitlyDenied'; } else if (resourceAnalysis.denyStatements.some((s) => s.principalMatch === 'AccountLevelMatch')) { resourceAnalysis.result = 'DeniedForAccount'; } else if (resourceAnalysis.allowStatements.some((s) => principalMatchOptions.includes(s.principalMatch))) { resourceAnalysis.result = 'Allowed'; } else if (resourceAnalysis.allowStatements.some((s) => s.principalMatch === 'AccountLevelMatch')) { resourceAnalysis.result = 'AllowedForAccount'; } else { resourceAnalysis.result = 'ImplicitlyDenied'; } return resourceAnalysis; } function analyzePermissionBoundaryPolicies(permissionBoundaries, request, simulationParameters) { if (!permissionBoundaries || permissionBoundaries.length === 0) { return undefined; } return analyzeIdentityPolicies(permissionBoundaries, request, simulationParameters); } function analyzeVpcEndpointPolicies(vpcEndPointPolicies, request, simulationParameters) { if (!vpcEndPointPolicies || vpcEndPointPolicies.length === 0) { return undefined; } return analyzeIdentityPolicies(vpcEndPointPolicies, request, simulationParameters); } function makeStatementExplain(statement, overallMatch, actionMatch, principalMatch, resourceMatch, conditionMatch, details) { return { effect: statement.effect(), identifier: statement.sid() || statement.index().toString(), matches: overallMatch, actionMatch, principalMatch, resourceMatch, conditionMatch: conditionMatch === 'Match', ...details }; } /** * Create an analysis of the ignored conditions in all statements. * * @param scpAnalysis the SCP analysis * @param rcpAnalysis the RCP analysis * @param identityAnalysis the identity analysis * @param resourceAnalysis the resource analysis * @param permissionBoundaryAnalysis the permission boundary analysis (optional) * @returns an object containing the ignored conditions for each analysis */ function ignoredConditionsAnalysis(scpAnalysis, rcpAnalysis, identityAnalysis, resourceAnalysis, permissionBoundaryAnalysis, endpointAnalysis) { const ignoredConditions = {}; addIgnoredConditionsToAnalysis(ignoredConditions, 'scp', scpAnalysis.ouAnalysis); addIgnoredConditionsToAnalysis(ignoredConditions, 'rcp', rcpAnalysis.ouAnalysis); addIgnoredConditionsToAnalysis(ignoredConditions, 'identity', [identityAnalysis]); addIgnoredConditionsToAnalysis(ignoredConditions, 'resource', [resourceAnalysis]); addIgnoredConditionsToAnalysis(ignoredConditions, 'permissionBoundary', permissionBoundaryAnalysis ? [permissionBoundaryAnalysis] : []); addIgnoredConditionsToAnalysis(ignoredConditions, 'endpointPolicy', endpointAnalysis ? [endpointAnalysis] : []); if (Object.keys(ignoredConditions).length > 0) { return ignoredConditions; } return undefined; } /** * Adds the specified ignored conditions to the analysis. * * @param analyses the analyses to map ignored conditions from * @returns the ignored conditions for allow and deny statements */ function addIgnoredConditionsToAnalysis(ignoredConditions, key, analyses) { const allow = []; const deny = []; const allStatements = analyses.flatMap((analysis) => [ ...analysis.allowStatements, ...analysis.denyStatements, ...analysis.unmatchedStatements ]); for (const statement of allStatements) { if (statement.ignoredConditions && statement.ignoredConditions.length > 0) { if (statement.statement.isAllow()) { allow.push(...statement.ignoredConditions.map((c) => ({ op: c.operation().value(), key: c.conditionKey(), values: c.conditionValues() }))); } else { deny.push(...statement.ignoredConditions.map((c) => ({ op: c.operation().value(), key: c.conditionKey(), values: c.conditionValues() }))); } } } if (allow.length === 0 && deny.length === 0) { return; } const newValue = {}; if (allow.length > 0) { newValue.allow = allow; } if (deny.length > 0) { newValue.deny = deny; } ignoredConditions[key] = newValue; } /** * Checks all analyses to see if any of them have statements that ignore the role session name. * * @param scpAnalysis the SCP analysis * @param rcpAnalysis the RCP analysis * @param identityAnalysis the identity analysis * @param resourceAnalysis the resource analysis * @param permissionBoundaryAnalysis the permission boundary analysis (optional) * @returns true if any analysis has statements that ignore the role session name, false otherwise */ function roleSessionNameIgnored(scpAnalysis, rcpAnalysis, identityAnalysis, resourceAnalysis, permissionBoundaryAnalysis) { return (scpAnalysis.ouAnalysis.some((ou) => ou.allowStatements.some((s) => s.ignoredRoleSessionName)) || scpAnalysis.ouAnalysis.some((ou) => ou.unmatchedStatements.some((s) => s.ignoredRoleSessionName)) || rcpAnalysis.ouAnalysis.some((ou) => ou.allowStatements.some((s) => s.ignoredRoleSessionName)) || rcpAnalysis.ouAnalysis.some((ou) => ou.unmatchedStatements.some((s) => s.ignoredRoleSessionName)) || identityAnalysis.allowStatements.some((s) => s.ignoredRoleSessionName) || identityAnalysis.unmatchedStatements.some((s) => s.ignoredRoleSessionName) || resourceAnalysis.allowStatements.some((s) => s.ignoredRoleSessionName) || resourceAnalysis.unmatchedStatements.some((s) => s.ignoredRoleSessionName) || permissionBoundaryAnalysis?.allowStatements.some((s) => s.ignoredRoleSessionName) || permissionBoundaryAnalysis?.unmatchedStatements.some((s) => s.ignoredRoleSessionName) || false); } //# sourceMappingURL=CoreSimulatorEngine.js.map