@cloud-copilot/iam-simulate
Version:
Simulate evaluation of AWS IAM policies
413 lines • 17.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestMatchesConditions = requestMatchesConditions;
exports.singleConditionMatchesRequest = singleConditionMatchesRequest;
exports.singleValueMatch = singleValueMatch;
exports.forAllValuesMatch = forAllValuesMatch;
exports.forAnyValueMatch = forAnyValueMatch;
const requestContext_js_1 = require("../requestContext.js");
const ArnEquals_js_1 = require("./arn/ArnEquals.js");
const ArnLike_js_1 = require("./arn/ArnLike.js");
const ArnNotEquals_js_1 = require("./arn/ArnNotEquals.js");
const ArnNotLike_js_1 = require("./arn/ArnNotLike.js");
const BinaryEquals_js_1 = require("./binary/BinaryEquals.js");
const Bool_js_1 = require("./boolean/Bool.js");
const DateEquals_js_1 = require("./date/DateEquals.js");
const DateGreaterThan_js_1 = require("./date/DateGreaterThan.js");
const DateGreaterThanEquals_js_1 = require("./date/DateGreaterThanEquals.js");
const DateLessThan_js_1 = require("./date/DateLessThan.js");
const DateLessThanEquals_js_1 = require("./date/DateLessThanEquals.js");
const DateNotEquals_js_1 = require("./date/DateNotEquals.js");
const IpAddress_js_1 = require("./ipaddress/IpAddress.js");
const NotIpAddress_js_1 = require("./ipaddress/NotIpAddress.js");
const NumericEquals_js_1 = require("./numeric/NumericEquals.js");
const NumericGreaterThan_js_1 = require("./numeric/NumericGreaterThan.js");
const NumericGreaterThanEquals_js_1 = require("./numeric/NumericGreaterThanEquals.js");
const NumericLessThan_js_1 = require("./numeric/NumericLessThan.js");
const NumericNotEquals_js_1 = require("./numeric/NumericNotEquals.js");
const StringEquals_js_1 = require("./string/StringEquals.js");
const StringEqualsIgnoreCase_js_1 = require("./string/StringEqualsIgnoreCase.js");
const StringLike_js_1 = require("./string/StringLike.js");
const StringNotEquals_js_1 = require("./string/StringNotEquals.js");
const StringNotEqualsIgnoreCase_js_1 = require("./string/StringNotEqualsIgnoreCase.js");
const StringNotLike_js_1 = require("./string/StringNotLike.js");
const allOperators = [
StringEquals_js_1.StringEquals,
StringNotEquals_js_1.StringNotEquals,
StringEqualsIgnoreCase_js_1.StringEqualsIgnoreCase,
StringNotEqualsIgnoreCase_js_1.StringNotEqualsIgnoreCase,
StringLike_js_1.StringLike,
StringNotLike_js_1.StringNotLike,
NumericEquals_js_1.NumericEquals,
NumericNotEquals_js_1.NumericNotEquals,
NumericLessThan_js_1.NumericLessThan,
NumericNotEquals_js_1.NumericNotEquals,
NumericGreaterThan_js_1.NumericGreaterThan,
NumericGreaterThanEquals_js_1.NumericGreaterThanEquals,
DateEquals_js_1.DateEquals,
DateNotEquals_js_1.DateNotEquals,
DateLessThan_js_1.DateLessThan,
DateLessThanEquals_js_1.DateLessThanEquals,
DateGreaterThan_js_1.DateGreaterThan,
DateGreaterThanEquals_js_1.DateGreaterThanEquals,
Bool_js_1.Bool,
BinaryEquals_js_1.BinaryEquals,
IpAddress_js_1.IpAddress,
NotIpAddress_js_1.NotIpAddress,
ArnLike_js_1.ArnLike,
ArnEquals_js_1.ArnEquals,
ArnNotLike_js_1.ArnNotLike,
ArnNotEquals_js_1.ArnNotEquals
];
const baseOperations = {};
for (const operator of allOperators) {
baseOperations[operator.name.toLowerCase()] = operator;
}
/**
* Evaluate a set of conditions against a request
*
* @param request the request to test
* @param conditions the conditions to test
* @returns Match if all conditions match, NoMatch if any do not. Also returns all the details of the evaluation
*/
function requestMatchesConditions(request, conditions, statementType, simulationParameters) {
const results = conditions.map((condition) => ({
condition,
explain: singleConditionMatchesRequest(request, condition, simulationParameters)
}));
const isIgnored = (c) => {
if (simulationParameters.simulationMode !== 'Discovery') {
return false;
}
if (simulationParameters.strictConditionKeys.has(c.condition.conditionKey().toLowerCase())) {
return false;
}
// In Allows we ignore conditions that do not match
if (statementType.toLowerCase() === 'allow') {
return !c.explain.matches;
}
// In Denies we ignore conditions that do match
if (statementType.toLowerCase() === 'deny') {
return c.explain.matches;
}
throw new Error(`Unexpected condition explain result in discovery mode, statementType: ${statementType}`);
};
const nonMatch = results.filter((r) => !isIgnored(r)).some((result) => !result.explain.matches);
const ignoredMatches = results
.filter((r) => isIgnored(r))
.some((result) => result.explain.matches);
return {
//If there is a non-match this is not ignored, it's a NoMatch
//If there are matches that are ignored, it is also a NoMatch,
// for instance in a Deny statement it may match a condition that is ignored,
// but we still want a no match so we can show under what conditions it would be allowed
matches: nonMatch || ignoredMatches ? 'NoMatch' : 'Match',
details: {
conditions: results.length == 0 ? undefined : results.map((r) => r.explain)
},
//Ignored conditions only matter if the non ignored fields all match
ignoredConditions: nonMatch ? undefined : ignoredConditions(results, isIgnored)
};
}
/**
* Get the list of conditions that were ignored during discovery mode, if any
*
* @param conditions the conditions that were evaluated with their explains
* @param statementType whether the statement is an allow or deny statement
* @param simulationParameters the general parameters for the simulation
* @returns an array of ignored conditions, or undefined if there are none
*/
function ignoredConditions(conditions, isIgnored) {
const ignoredConditions = conditions.filter(isIgnored);
if (ignoredConditions.length > 0) {
return ignoredConditions.map((r) => r.condition);
}
return undefined;
}
/**
* Checks to see if a single condition matches a request
*
* @param request the request to test
* @param condition the condition to test
* @returns the result of evaluating the condition
*/
function singleConditionMatchesRequest(request, condition, simulationParameters) {
const key = condition.conditionKey();
const baseOperation = baseOperations[condition.operation().baseOperator().toLowerCase()];
const keyExists = request.contextKeyExists(key);
const keyValue = keyExists ? request.getContextKeyValue(key) : undefined;
if (condition.operation().value().toLowerCase() == 'null' ||
condition.operation().baseOperator()?.toLowerCase() == 'null') {
return testNull(condition, keyExists);
}
if (condition.operation().setOperator()) {
const setOperator = condition.operation().setOperator();
if (setOperator === 'ForAnyValue') {
return forAnyValueMatch(request, condition, keyValue, baseOperation);
}
else if (setOperator === 'ForAllValues') {
return forAllValuesMatch(request, condition, keyValue, baseOperation);
}
else {
throw new Error(`Unknown set operator: ${setOperator}`);
}
}
return singleValueMatch(request, condition, baseOperation, keyValue);
}
/**
* Tests a condition with a null operator
*
* @param condition the condition to test
* @param keyExists whether the key exists in the request
* @returns the result of evaluating the null operator
*/
function testNull(condition, keyExists) {
const goalValue = keyExists ? 'false' : 'true';
const conditionValues = condition.conditionValues().map((value) => {
return {
value,
matches: value.toLowerCase() === goalValue
};
});
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: condition.valueIsArray() ? conditionValues : conditionValues[0],
matches: conditionValues.some((value) => value.matches)
};
}
function singleValueMatch(request, condition, baseOperation, keyValue) {
const isNotOperator = condition.operation().baseOperator().toLowerCase().includes('not');
if (condition.operation().isIfExists() || isNotOperator) {
//Check if it exists, return true if it doesn't
//Double check what happens here if the key is not a valid key or is of the wrong type
if (!keyValue) {
const valueExplains = condition.conditionValues().map((value) => ({
value,
matches: true
}));
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: condition.valueIsArray() ? valueExplains : valueExplains[0],
matches: true,
matchedBecauseMissing: true,
resolvedConditionKeyValue: keyValue
};
}
}
if (!keyValue || !keyValue.isStringValue()) {
//Set operator is required for a multi-value key
//Confirmed this at re:Inforce 2025 IAM431.
const valueExplains = condition.conditionValues().map((value) => ({
value,
matches: false
}));
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: condition.valueIsArray() ? valueExplains : valueExplains[0],
matches: false,
failedBecauseMissing: !keyValue,
failedBecauseArray: keyValue?.isArrayValue()
};
}
if (!baseOperation) {
const valueExplains = condition.conditionValues().map((value) => ({
value,
matches: false
}));
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: condition.valueIsArray() ? valueExplains : valueExplains[0],
matches: false,
missingOperator: true
};
}
const { matches, explains } = baseOperation.matches(request, keyValue.value, condition.conditionValues());
return {
matches,
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: condition.valueIsArray() ? explains : explains[0],
resolvedConditionKeyValue: keyValue.value
};
}
/**
* Tests a condition with a ForAllValues set operator
*
* @param request the request to test
* @param condition the condition with ForAllValues set operator
* @param keyExists whether the key exists in the request
* @param keyValue the value of the key in the request
* @param baseOperation the base operation to test the key against
* @returns the result of evaluating the ForAllValues set operator
*/
function forAllValuesMatch(request, condition, keyValue, baseOperation) {
const matchingValueExplains = condition
.conditionValues()
.map((value) => ({
value,
matches: true
}));
const notMatchingValueExplains = condition
.conditionValues()
.map((value) => ({
value,
matches: false
}));
if (!keyValue) {
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: matchingValueExplains,
matches: true,
matchedBecauseMissing: true
};
}
// If the key only has a single value, convert it to an array to process
if (keyValue.isStringValue()) {
keyValue = new requestContext_js_1.ContextKeyImpl(keyValue.name, [keyValue.value]);
}
if (!keyValue.isArrayValue()) {
throw new Error('Key value is not an array, this is a bug.');
}
if (!baseOperation) {
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: notMatchingValueExplains,
matches: false,
missingOperator: true
};
}
const valueExplains = keyValue.values.map((value) => {
const { matches, explains } = baseOperation.matches(request, value, condition.conditionValues());
return {
requestValue: value,
matches,
explains
};
});
const anyNonMatches = valueExplains.some((valueExplain) => !valueExplain.matches);
const overallMatch = !anyNonMatches;
const unmatchedValues = [];
const explains = {};
for (const valueExplain of valueExplains) {
if (!baseOperation.isNegative && !valueExplain.matches) {
unmatchedValues.push(valueExplain.requestValue);
}
else if (baseOperation.isNegative && valueExplain.matches) {
unmatchedValues.push(valueExplain.requestValue);
}
for (const explain of valueExplain.explains) {
let theExplain = explains[explain.value];
if (!theExplain) {
explains[explain.value] = {
value: explain.value,
matches: overallMatch
};
theExplain = explains[explain.value];
}
if (explain.matches && !baseOperation.isNegative) {
theExplain.matchingValues = theExplain.matchingValues || [];
theExplain.matchingValues.push(valueExplain.requestValue);
}
else if (!explain.matches && baseOperation.isNegative) {
theExplain.negativeMatchingValues = theExplain.negativeMatchingValues || [];
theExplain.negativeMatchingValues.push(valueExplain.requestValue);
}
}
}
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: Object.values(explains),
matches: overallMatch,
unmatchedValues
};
}
/**
* Test a condition with a ForAnyValue set operator
*
* @param request the request to test
* @param condition the condition with ForAnyValue set operator
* @param keyExists whether the key exists in the request
* @param keyValue the value of the key in the request
* @param baseOperation the base operation to test the key against
* @returns the result of evaluating the ForAnyValue set operator
*/
function forAnyValueMatch(request, condition, keyValue, baseOperation) {
const failedValueExplains = condition.conditionValues().map((value) => ({
value,
matches: false
}));
if (!keyValue) {
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: failedValueExplains,
matches: false,
failedBecauseMissing: true
};
// return 'NoMatch'
}
// If the key only has a single value, convert it to an array to process
if (keyValue.isStringValue()) {
keyValue = new requestContext_js_1.ContextKeyImpl(keyValue.name, [keyValue.value]);
}
if (!keyValue.isArrayValue()) {
throw new Error('Key value is not an array, this is a bug.');
}
if (!baseOperation) {
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: failedValueExplains,
matches: false,
missingOperator: true
};
}
const valueExplains = keyValue.values.map((value) => {
const { matches, explains } = baseOperation.matches(request, value, condition.conditionValues());
return {
requestValue: value,
matches,
explains
};
});
const overallMatch = valueExplains.some((valueExplain) => valueExplain.matches);
const unmatchedValues = [];
const explains = {};
for (const valueExplain of valueExplains) {
if (!baseOperation.isNegative && !valueExplain.matches) {
unmatchedValues.push(valueExplain.requestValue);
}
else if (baseOperation.isNegative && valueExplain.matches) {
unmatchedValues.push(valueExplain.requestValue);
}
for (const explain of valueExplain.explains) {
let theExplain = explains[explain.value];
if (!theExplain) {
explains[explain.value] = {
value: explain.value,
matches: overallMatch
};
theExplain = explains[explain.value];
}
if (explain.matches) {
theExplain.matchingValues = theExplain.matchingValues || [];
theExplain.matchingValues.push(valueExplain.requestValue);
}
}
}
return {
operator: condition.operation().value(),
conditionKeyValue: condition.conditionKey(),
values: Object.values(explains),
matches: overallMatch,
unmatchedValues
};
}
//# sourceMappingURL=condition.js.map