UNPKG

@aws-amplify/cli-internal

Version:
213 lines • 11.4 kB
"use strict"; 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