@aws-amplify/cli-internal
Version:
Amplify CLI
213 lines • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectStackDriftRecursive = exports.isAmplifyTriggerPolicyDrift = exports.isAmplifyRestApiDescriptionDrift = exports.detectStackDrift = void 0;
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const categories_1 = require("../gen2-migration/_common/categories");
const utils_1 = require("../gen2-migration/_common/utils");
const FALSE_POSITIVE_FILTERS = [isAmplifyAuthRoleDenyToAllowChange, isAmplifyRestApiDescriptionDrift, isAmplifyTriggerPolicyDrift];
const isDrifted = (d) => d.StackResourceDriftStatus === client_cloudformation_1.StackResourceDriftStatus.MODIFIED || d.StackResourceDriftStatus === client_cloudformation_1.StackResourceDriftStatus.DELETED;
function collectSkippedStacks(node, result = []) {
if (node.skippedChildren)
result.push(...node.skippedChildren);
for (const child of node.children)
collectSkippedStacks(child, result);
return result;
}
async function detectStackDrift(cfn, stackName, print) {
var _a;
const driftDetection = await cfn.send(new client_cloudformation_1.DetectStackDriftCommand({
StackName: stackName,
}));
print.debug(`Detecting drift with ID ${driftDetection.StackDriftDetectionId} for stack ${stackName}...`);
const driftStatus = await waitForDriftDetection(cfn, driftDetection.StackDriftDetectionId, print);
if ((driftStatus === null || driftStatus === void 0 ? void 0 : driftStatus.StackDriftStatus) === 'UNKNOWN') {
print.debug('Stack drift status is UNKNOWN. This may occur when CloudFormation is unable to detect drift for at least one resource and all other resources are IN_SYNC.\n' +
`Reason: ${(_a = driftStatus.DetectionStatusReason) !== null && _a !== void 0 ? _a : 'No reason provided'}`);
}
const allDrifts = [];
const paginator = (0, client_cloudformation_1.paginateDescribeStackResourceDrifts)({ client: cfn, pageSize: 100 }, { StackName: stackName });
for await (const page of paginator) {
if (page.StackResourceDrifts)
allDrifts.push(...page.StackResourceDrifts);
}
const filteredDrifts = allDrifts.map((drift) => {
if (drift.StackResourceDriftStatus === client_cloudformation_1.StackResourceDriftStatus.MODIFIED &&
drift.PropertyDifferences &&
drift.PropertyDifferences.length > 0) {
drift.PropertyDifferences = drift.PropertyDifferences.filter((propDiff) => !FALSE_POSITIVE_FILTERS.some((filter) => filter(drift, propDiff, print)));
if (drift.PropertyDifferences.length === 0) {
drift.StackResourceDriftStatus = client_cloudformation_1.StackResourceDriftStatus.IN_SYNC;
}
}
return drift;
});
return { drifts: filteredDrifts, driftDetectionId: driftDetection.StackDriftDetectionId };
}
exports.detectStackDrift = detectStackDrift;
function isAmplifyAuthRoleDenyToAllowChange(_drift, propDiff, print) {
var _a, _b;
if (!propDiff.PropertyPath || !propDiff.PropertyPath.includes('AssumeRolePolicyDocument')) {
return false;
}
const expectedValue = propDiff.ExpectedValue;
const actualValue = propDiff.ActualValue;
if (typeof expectedValue === 'string' && typeof actualValue === 'string') {
try {
const expectedJson = JSON.parse(expectedValue);
const actualJson = JSON.parse(actualValue);
if (expectedJson.Statement && actualJson.Statement && Array.isArray(expectedJson.Statement) && Array.isArray(actualJson.Statement)) {
for (let i = 0; i < Math.max(expectedJson.Statement.length, actualJson.Statement.length); i++) {
const expectedStatement = expectedJson.Statement[i];
const actualStatement = actualJson.Statement[i];
if (expectedStatement &&
actualStatement &&
expectedStatement.Effect === 'Deny' &&
actualStatement.Effect === 'Allow' &&
((_a = expectedStatement.Principal) === null || _a === void 0 ? void 0 : _a.Federated) === 'cognito-identity.amazonaws.com' &&
((_b = actualStatement.Principal) === null || _b === void 0 ? void 0 : _b.Federated) === 'cognito-identity.amazonaws.com') {
return true;
}
}
}
}
catch (e) {
print.debug(`Failed to parse AssumeRolePolicyDocument JSON: ${e.message || 'Unknown error'}`);
return false;
}
}
return false;
}
function isAmplifyRestApiDescriptionDrift(drift, propDiff, print) {
if (drift.ResourceType !== 'AWS::ApiGateway::RestApi')
return false;
if (!propDiff.PropertyPath || propDiff.PropertyPath !== '/Description')
return false;
if (propDiff.ActualValue === 'null' && propDiff.ExpectedValue === '') {
print.debug(`Filtering false positive: REST API Description drift on ${drift.LogicalResourceId}`);
return true;
}
return false;
}
exports.isAmplifyRestApiDescriptionDrift = isAmplifyRestApiDescriptionDrift;
function isAmplifyTriggerPolicyDrift(drift, propDiff, print) {
var _a;
if (drift.ResourceType !== 'AWS::IAM::Role')
return false;
if (!propDiff.PropertyPath || !/\/Policies\/\d+/.test(propDiff.PropertyPath))
return false;
if (propDiff.ExpectedValue !== 'null')
return false;
try {
const actualPolicy = JSON.parse((_a = propDiff.ActualValue) !== null && _a !== void 0 ? _a : '');
const policyName = actualPolicy.PolicyName;
const policyDoc = JSON.parse(actualPolicy.PolicyDocument);
const actions = new Set(policyDoc.Statement.flatMap((s) => [s.Action].flat()));
const cognitoActions = ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:GetGroup', 'cognito-idp:CreateGroup'];
const isCognitoTriggerPolicy = policyName.endsWith('AddToGroupCognito') && cognitoActions.every((a) => actions.has(a));
const s3Actions = ['s3:ListBucket', 's3:PutObject', 's3:GetObject', 's3:DeleteObject'];
const isS3TriggerPolicy = policyName === 'amplify-lambda-execution-policy-storage' && s3Actions.every((a) => actions.has(a));
if (isCognitoTriggerPolicy || isS3TriggerPolicy) {
print.debug(`Filtering false positive: trigger policy drift on ${drift.LogicalResourceId} (${policyName})`);
return true;
}
}
catch (e) {
print.debug(`Failed to parse trigger policy JSON: ${e.message || 'Unknown error'}`);
return false;
}
return false;
}
exports.isAmplifyTriggerPolicyDrift = isAmplifyTriggerPolicyDrift;
async function waitForDriftDetection(cfn, driftDetectionId, print) {
var _a;
const maxWaitForDrift = 300000;
const timeBetweenOutputs = 10000;
const timeBetweenApiCalls = 2000;
const deadline = Date.now() + maxWaitForDrift;
let checkIn = Date.now() + timeBetweenOutputs;
while (true) {
const response = await cfn.send(new client_cloudformation_1.DescribeStackDriftDetectionStatusCommand({
StackDriftDetectionId: driftDetectionId,
}));
if (response.DetectionStatus === 'DETECTION_COMPLETE') {
return response;
}
if (response.DetectionStatus === 'DETECTION_FAILED') {
throw new amplify_cli_core_1.AmplifyError('CloudFormationTemplateError', {
message: `Drift detection failed: ${(_a = response.DetectionStatusReason) !== null && _a !== void 0 ? _a : 'No reason provided'}`,
resolution: 'Check CloudFormation console for more details or try again.',
});
}
if (Date.now() > deadline) {
throw new amplify_cli_core_1.AmplifyError('CloudFormationTemplateError', {
message: `Drift detection timed out after ${maxWaitForDrift / 1000} seconds.`,
resolution: 'The stack may be too large or AWS may be experiencing issues. Try again later.',
});
}
if (Date.now() > checkIn) {
print.debug('Waiting for drift detection to complete...');
checkIn = Date.now() + timeBetweenOutputs;
}
await new Promise((resolve) => setTimeout(resolve, timeBetweenApiCalls));
}
}
async function buildDriftNode(cfn, physicalName, logicalId, print, parentCategory) {
var _a, _b;
print.push((0, utils_1.extractStackNameFromId)(physicalName));
const { drifts: allDrifts, driftDetectionId } = await detectStackDrift(cfn, physicalName, print);
print.pop();
const drifts = allDrifts.filter(isDrifted);
let category;
if (logicalId === null) {
category = 'Core Infrastructure';
}
else {
category = (0, categories_1.extractCategory)(logicalId);
if (category === 'Other' && parentCategory) {
category = parentCategory;
}
}
const stackResources = await cfn.send(new client_cloudformation_1.DescribeStackResourcesCommand({ StackName: physicalName }));
const nestedStacks = ((_a = stackResources.StackResources) === null || _a === void 0 ? void 0 : _a.filter((resource) => resource.ResourceType === 'AWS::CloudFormation::Stack')) || [];
if (nestedStacks.length > 0) {
print.debug(`Found ${nestedStacks.length} nested stack(s) in ${logicalId !== null && logicalId !== void 0 ? logicalId : physicalName}`);
}
const children = [];
const skippedChildren = [];
for (const nested of nestedStacks) {
if (!nested.LogicalResourceId || !nested.PhysicalResourceId)
continue;
if ((_b = nested.ResourceStatus) === null || _b === void 0 ? void 0 : _b.includes('DELETE')) {
print.debug(`Skipping deleted nested stack: ${nested.LogicalResourceId}`);
continue;
}
try {
print.debug(`Checking drift for nested stack: ${nested.LogicalResourceId}`);
const childNode = await buildDriftNode(cfn, nested.PhysicalResourceId, nested.LogicalResourceId, print, category);
children.push(childNode);
print.debug(`buildDriftNode.nested: ${nested.LogicalResourceId}, ${childNode.drifts.length} direct resources, ${childNode.children.length} sub-stacks`);
}
catch (error) {
print.warn(`Failed to check drift for nested stack ${nested.LogicalResourceId}: ${error.message}`);
skippedChildren.push(nested.LogicalResourceId);
}
}
return {
logicalId: logicalId !== null && logicalId !== void 0 ? logicalId : physicalName,
category,
drifts,
driftDetectionId,
children,
skippedChildren: skippedChildren.length > 0 ? skippedChildren : undefined,
};
}
async function detectStackDriftRecursive(cfn, stackName, print) {
print.debug(`detectStackDriftRecursive: ${stackName}`);
const root = await buildDriftNode(cfn, stackName, null, print);
const skippedStacks = collectSkippedStacks(root);
print.debug(`detectStackDriftRecursive.complete: ${stackName}`);
return { root, skippedStacks, incomplete: skippedStacks.length > 0 };
}
exports.detectStackDriftRecursive = detectStackDriftRecursive;
//# sourceMappingURL=detect-stack-drift.js.map