@cloud-copilot/iam-simulate
Version:
Simulate evaluation of AWS IAM policies
421 lines • 20.7 kB
JavaScript
;
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