From bc359c6225527846fc9ca5104990f877375a6562 Mon Sep 17 00:00:00 2001 From: Sai Ray Date: Fri, 13 Mar 2026 17:09:41 -0400 Subject: [PATCH 1/3] chore: add drift filter for false-positive drifts --- docs/amplify-cli/src/drift.md | 25 ++- .../detect-stack-drift.test.ts | 157 ++++++++++++++++++ .../drift-detection/detect-stack-drift.ts | 66 +++++++- 3 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts diff --git a/docs/amplify-cli/src/drift.md b/docs/amplify-cli/src/drift.md index 6d12a1179f7..0f7ee6db6eb 100644 --- a/docs/amplify-cli/src/drift.md +++ b/docs/amplify-cli/src/drift.md @@ -100,7 +100,11 @@ graph TB 2. Calls AWS `DetectStackDrift` API for each stack 3. Polls for completion (5-minute timeout, 2-second intervals) 4. Retrieves detailed drift information for all resources -5. Filters out known false positives (e.g., Auth role Deny→Allow changes) +5. Filters out known false positives: + - Auth role Deny→Allow changes (Cognito Identity Pool) + - REST API Description property (null vs empty) + - Auth trigger policies on Lambda execution roles (Cognito trigger policies added during push) + - S3 trigger policies on Lambda execution roles (S3 trigger policies added during push) 6. Filters to only drifted resources (MODIFIED or DELETED) at detection time **Example drift detected:** @@ -316,6 +320,25 @@ function isAmplifyAuthRoleDenyToAllowChange(propDiff, print): boolean { } ``` +### REST API & Trigger Policy Filtering (False Positives) + +Amplify's push pipeline introduces drift that should not be reported: + +```typescript +// From detect-stack-drift.ts + +// REST API Description: null vs empty mismatch +function isAmplifyRestApiDescriptionDrift(drift, propDiff, print): boolean { + // Filters /Description on AWS::ApiGateway::RestApi where one side is null +} + +// Auth/S3 trigger policies added to Lambda execution roles during push +function isAmplifyTriggerPolicyDrift(drift, propDiff, print): boolean { + // Filters /Policies/N on AWS::IAM::Role where ExpectedValue is null + // and ActualValue contains known Cognito or S3 trigger policy content +} +``` + ### Graceful Degradation Phases can be skipped without failing the entire operation, with results including skip reasons: diff --git a/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts b/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts new file mode 100644 index 00000000000..a3f3487bbe4 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts @@ -0,0 +1,157 @@ +import { type StackResourceDrift, type PropertyDifference } from '@aws-sdk/client-cloudformation'; +import { isAmplifyRestApiDescriptionDrift, isAmplifyTriggerPolicyDrift } from '../../../commands/drift-detection/detect-stack-drift'; +import type { Printer } from '@aws-amplify/amplify-prompts'; + +const mockPrinter: Printer = { + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + blankLine: jest.fn(), + success: jest.fn(), + error: jest.fn(), +}; + +function makeDrift(overrides: Partial = {}): StackResourceDrift { + return { + StackId: 'arn:aws:cloudformation:us-east-1:123:stack/test/guid', + LogicalResourceId: 'TestResource', + ResourceType: 'AWS::Lambda::Function', + StackResourceDriftStatus: 'MODIFIED', + PropertyDifferences: [], + ...overrides, + } as StackResourceDrift; +} + +function makePropDiff(overrides: Partial = {}): PropertyDifference { + return { + PropertyPath: '/SomeProperty', + ExpectedValue: 'old', + ActualValue: 'new', + DifferenceType: 'NOT_EQUAL', + ...overrides, + }; +} + +describe('isAmplifyRestApiDescriptionDrift', () => { + beforeEach(() => jest.clearAllMocks()); + + it('filters RestApi Description drift where actual is null and expected is empty', () => { + const drift = makeDrift({ ResourceType: 'AWS::ApiGateway::RestApi', LogicalResourceId: 'apinutritionapi' }); + const propDiff = makePropDiff({ PropertyPath: '/Description', ExpectedValue: '', ActualValue: 'null' }); + expect(isAmplifyRestApiDescriptionDrift(drift, propDiff, mockPrinter)).toBe(true); + }); + + it('ignores wrong property, wrong resource type, and genuine description changes', () => { + const drift = makeDrift({ ResourceType: 'AWS::ApiGateway::RestApi' }); + // wrong property + expect( + isAmplifyRestApiDescriptionDrift(drift, makePropDiff({ PropertyPath: '/Name', ExpectedValue: '', ActualValue: 'null' }), mockPrinter), + ).toBe(false); + // wrong resource type + expect( + isAmplifyRestApiDescriptionDrift( + makeDrift({ ResourceType: 'AWS::Lambda::Function' }), + makePropDiff({ PropertyPath: '/Description', ExpectedValue: '', ActualValue: 'null' }), + mockPrinter, + ), + ).toBe(false); + // both non-null + expect( + isAmplifyRestApiDescriptionDrift( + drift, + makePropDiff({ PropertyPath: '/Description', ExpectedValue: 'old', ActualValue: 'new' }), + mockPrinter, + ), + ).toBe(false); + }); +}); + +describe('isAmplifyTriggerPolicyDrift', () => { + beforeEach(() => jest.clearAllMocks()); + + it('filters Cognito trigger policy drift (AdminAddUserToGroup)', () => { + const drift = makeDrift({ ResourceType: 'AWS::IAM::Role', LogicalResourceId: 'LambdaExecutionRole' }); + const policyValue = JSON.stringify({ + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:GetGroup', 'cognito-idp:CreateGroup'], + Resource: 'arn:aws:cognito-idp:us-east-1:123:userpool/us-east-1_abc', + }, + ], + }), + PolicyName: 'AddToGroupCognito', + }); + const propDiff = makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'null', ActualValue: policyValue }); + expect(isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toBe(true); + }); + + it('filters S3 storage trigger policy drift', () => { + const drift = makeDrift({ ResourceType: 'AWS::IAM::Role', LogicalResourceId: 'LambdaExecutionRole' }); + const policyValue = JSON.stringify({ + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { Effect: 'Allow', Action: 's3:ListBucket', Resource: 'arn:aws:s3:::storagebucket-main' }, + { + Effect: 'Allow', + Action: ['s3:PutObject', 's3:GetObject', 's3:ListBucket', 's3:DeleteObject'], + Resource: 'arn:aws:s3:::storagebucket-main/*', + }, + ], + }), + PolicyName: 'amplify-lambda-execution-policy-storage', + }); + const propDiff = makePropDiff({ PropertyPath: '/Policies/1', ExpectedValue: 'null', ActualValue: policyValue }); + expect(isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toBe(true); + }); + + it('ignores wrong resource type, wrong property path, and non-null expected value', () => { + // wrong resource type + expect( + isAmplifyTriggerPolicyDrift( + makeDrift({ ResourceType: 'AWS::IAM::Policy' }), + makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'null', ActualValue: '{}' }), + mockPrinter, + ), + ).toBe(false); + // wrong property path + expect( + isAmplifyTriggerPolicyDrift( + makeDrift({ ResourceType: 'AWS::IAM::Role' }), + makePropDiff({ PropertyPath: '/AssumeRolePolicyDocument', ExpectedValue: 'null', ActualValue: '{}' }), + mockPrinter, + ), + ).toBe(false); + // expected is not null (policy already existed in template) + const policyValue = JSON.stringify({ PolicyDocument: '{"Statement":[]}', PolicyName: 'amplify-lambda-execution-policy-storage' }); + expect( + isAmplifyTriggerPolicyDrift( + makeDrift({ ResourceType: 'AWS::IAM::Role' }), + makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'some-existing-policy', ActualValue: policyValue }), + mockPrinter, + ), + ).toBe(false); + }); + + it('does not filter unknown policy content', () => { + const drift = makeDrift({ ResourceType: 'AWS::IAM::Role' }); + const policyValue = JSON.stringify({ + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ Effect: 'Allow', Action: 'dynamodb:PutItem', Resource: '*' }], + }), + PolicyName: 'SomeCustomPolicy', + }); + const propDiff = makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'null', ActualValue: policyValue }); + expect(isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toBe(false); + }); + + it('throws on malformed JSON', () => { + const drift = makeDrift({ ResourceType: 'AWS::IAM::Role' }); + const propDiff = makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'null', ActualValue: 'not-json' }); + expect(() => isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toThrow(); + }); +}); diff --git a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts index b1ff2ed4581..1d1eb0e8bc4 100644 --- a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts +++ b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts @@ -92,7 +92,7 @@ export async function detectStackDrift( if (page.StackResourceDrifts) allDrifts.push(...page.StackResourceDrifts); } - // Filter out known Amplify Auth IdP Deny→Allow changes + // Filter out known Amplify-introduced false positives const filteredDrifts = allDrifts.map((drift) => { if ( drift.StackResourceDriftStatus === StackResourceDriftStatus.MODIFIED && @@ -100,7 +100,10 @@ export async function detectStackDrift( drift.PropertyDifferences.length > 0 ) { drift.PropertyDifferences = drift.PropertyDifferences.filter((propDiff) => { - return !isAmplifyAuthRoleDenyToAllowChange(propDiff, print); + if (isAmplifyAuthRoleDenyToAllowChange(propDiff, print)) return false; + if (isAmplifyRestApiDescriptionDrift(drift, propDiff, print)) return false; + if (isAmplifyTriggerPolicyDrift(drift, propDiff, print)) return false; + return true; }); if (drift.PropertyDifferences.length === 0) { @@ -163,7 +166,64 @@ function isAmplifyAuthRoleDenyToAllowChange(propDiff: PropertyDifference, print: } /** - * Wait for a drift detection operation to complete + * Check if a property difference is a REST API Description false positive. + * Amplify may set Description during template generation but the deployed + * resource reports it differently (or as null). + */ +export function isAmplifyRestApiDescriptionDrift(drift: StackResourceDrift, propDiff: PropertyDifference, print: Printer): boolean { + if (drift.ResourceType !== 'AWS::ApiGateway::RestApi') return false; + if (!propDiff.PropertyPath || propDiff.PropertyPath !== '/Description') return false; + + // The known false positive: actual is null, expected is empty + if (propDiff.ActualValue === 'null' && propDiff.ExpectedValue === '') { + print.debug(`Filtering false positive: REST API Description drift on ${drift.LogicalResourceId}`); + return true; + } + + return false; +} + +/** + * Check if a property difference is an Amplify trigger policy false positive. + * Auth triggers and S3 storage triggers dynamically attach IAM policies to + * Lambda execution roles during push. These appear as /Policies/N diffs where + * ExpectedValue is null and ActualValue contains a known Amplify policy. + */ +export function isAmplifyTriggerPolicyDrift(drift: StackResourceDrift, propDiff: PropertyDifference, print: Printer): boolean { + if (drift.ResourceType !== 'AWS::IAM::Role') return false; + if (!propDiff.PropertyPath || !/\/Policies\/\d+/.test(propDiff.PropertyPath)) return false; + + // The template has null, the deployed resource has the policy + if (propDiff.ExpectedValue !== 'null') return false; + + const actualPolicy = JSON.parse(propDiff.ActualValue ?? ''); + const policyName: string = actualPolicy.PolicyName; + const policyDocStr: string = actualPolicy.PolicyDocument; + + // Auth trigger policies: known Cognito trigger policy pattern + const isCognitoTriggerPolicy = + policyName === 'AddToGroupCognito' && + policyDocStr.includes('cognito-idp:AdminAddUserToGroup') && + policyDocStr.includes('cognito-idp:GetGroup') && + policyDocStr.includes('cognito-idp:CreateGroup'); + + // S3 storage trigger policies: known S3 trigger policy pattern + const isS3TriggerPolicy = + policyName === 'amplify-lambda-execution-policy-storage' && + policyDocStr.includes('s3:ListBucket') && + policyDocStr.includes('s3:PutObject') && + policyDocStr.includes('s3:GetObject') && + policyDocStr.includes('s3:DeleteObject'); + + if (isCognitoTriggerPolicy || isS3TriggerPolicy) { + print.debug(`Filtering false positive: trigger policy drift on ${drift.LogicalResourceId} (${policyName})`); + return true; + } + + return false; +} + +/** * Based on CDK's polling strategy: 5-minute timeout, 2-second polling interval, 10-second user feedback */ async function waitForDriftDetection( From cd13218de1f5f11720137d43d9336023172550e0 Mon Sep 17 00:00:00 2001 From: Sai Ray Date: Mon, 23 Mar 2026 14:52:43 -0400 Subject: [PATCH 2/3] chore: apply suggestions from @9pace --- .../amplify-cli/src/commands/drift.md | 11 ++++-- .../drift-detection/detect-stack-drift.ts | 35 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/packages/amplify-cli/src/commands/drift.md b/docs/packages/amplify-cli/src/commands/drift.md index 0f7ee6db6eb..f3fb1f15cce 100644 --- a/docs/packages/amplify-cli/src/commands/drift.md +++ b/docs/packages/amplify-cli/src/commands/drift.md @@ -325,7 +325,12 @@ function isAmplifyAuthRoleDenyToAllowChange(propDiff, print): boolean { Amplify's push pipeline introduces drift that should not be reported: ```typescript -// From detect-stack-drift.ts +// From detect-stack-drift.ts — all filters are registered in a single array +const FALSE_POSITIVE_FILTERS = [ + isAmplifyAuthRoleDenyToAllowChange, + isAmplifyRestApiDescriptionDrift, + isAmplifyTriggerPolicyDrift, +] as const; // REST API Description: null vs empty mismatch function isAmplifyRestApiDescriptionDrift(drift, propDiff, print): boolean { @@ -334,8 +339,8 @@ function isAmplifyRestApiDescriptionDrift(drift, propDiff, print): boolean { // Auth/S3 trigger policies added to Lambda execution roles during push function isAmplifyTriggerPolicyDrift(drift, propDiff, print): boolean { - // Filters /Policies/N on AWS::IAM::Role where ExpectedValue is null - // and ActualValue contains known Cognito or S3 trigger policy content + // Parses PolicyDocument JSON and checks actions via Set containment + // against known Cognito or S3 trigger policy patterns } ``` diff --git a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts index 1d1eb0e8bc4..8e1507fa079 100644 --- a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts +++ b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts @@ -17,6 +17,12 @@ import { AmplifyError } from '@aws-amplify/amplify-cli-core'; import { extractCategory } from '../gen2-migration/categories'; import type { Printer } from '@aws-amplify/amplify-prompts'; +/** + * Known false-positive filters applied to Phase 1 drift results. + * Add new filters here instead of modifying the detection loop. + */ +const FALSE_POSITIVE_FILTERS = [isAmplifyAuthRoleDenyToAllowChange, isAmplifyRestApiDescriptionDrift, isAmplifyTriggerPolicyDrift] as const; + /** * Enriched drift tree node — one per stack (root or nested) */ @@ -99,12 +105,9 @@ export async function detectStackDrift( drift.PropertyDifferences && drift.PropertyDifferences.length > 0 ) { - drift.PropertyDifferences = drift.PropertyDifferences.filter((propDiff) => { - if (isAmplifyAuthRoleDenyToAllowChange(propDiff, print)) return false; - if (isAmplifyRestApiDescriptionDrift(drift, propDiff, print)) return false; - if (isAmplifyTriggerPolicyDrift(drift, propDiff, print)) return false; - return true; - }); + drift.PropertyDifferences = drift.PropertyDifferences.filter( + (propDiff) => !FALSE_POSITIVE_FILTERS.some((filter) => filter(drift, propDiff, print)), + ); if (drift.PropertyDifferences.length === 0) { drift.StackResourceDriftStatus = StackResourceDriftStatus.IN_SYNC; @@ -119,7 +122,7 @@ export async function detectStackDrift( /** * Check if a property difference is an Amplify auth role Deny→Allow change (intended drift) */ -function isAmplifyAuthRoleDenyToAllowChange(propDiff: PropertyDifference, print: Printer): boolean { +function isAmplifyAuthRoleDenyToAllowChange(_drift: StackResourceDrift, propDiff: PropertyDifference, print: Printer): boolean { // Check if this is an AssumeRolePolicyDocument change if (!propDiff.PropertyPath || !propDiff.PropertyPath.includes('AssumeRolePolicyDocument')) { return false; @@ -198,22 +201,16 @@ export function isAmplifyTriggerPolicyDrift(drift: StackResourceDrift, propDiff: const actualPolicy = JSON.parse(propDiff.ActualValue ?? ''); const policyName: string = actualPolicy.PolicyName; - const policyDocStr: string = actualPolicy.PolicyDocument; + const policyDoc = JSON.parse(actualPolicy.PolicyDocument as string); + const actions = new Set(policyDoc.Statement.flatMap((s) => [s.Action].flat())); // Auth trigger policies: known Cognito trigger policy pattern - const isCognitoTriggerPolicy = - policyName === 'AddToGroupCognito' && - policyDocStr.includes('cognito-idp:AdminAddUserToGroup') && - policyDocStr.includes('cognito-idp:GetGroup') && - policyDocStr.includes('cognito-idp:CreateGroup'); + const cognitoActions = ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:GetGroup', 'cognito-idp:CreateGroup']; + const isCognitoTriggerPolicy = policyName === 'AddToGroupCognito' && cognitoActions.every((a) => actions.has(a)); // S3 storage trigger policies: known S3 trigger policy pattern - const isS3TriggerPolicy = - policyName === 'amplify-lambda-execution-policy-storage' && - policyDocStr.includes('s3:ListBucket') && - policyDocStr.includes('s3:PutObject') && - policyDocStr.includes('s3:GetObject') && - policyDocStr.includes('s3:DeleteObject'); + 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})`); From 64cffee8c2bf95d2e07e3efa357f57efb58581ea Mon Sep 17 00:00:00 2001 From: Sai Ray Date: Wed, 25 Mar 2026 15:39:19 -0400 Subject: [PATCH 3/3] chore: add error handling --- .../detect-stack-drift.test.ts | 4 +- .../drift-detection/detect-stack-drift.ts | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts b/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts index a67be0895f3..9043e5b217a 100644 --- a/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts +++ b/packages/amplify-cli/src/__tests__/commands/drift-detection/detect-stack-drift.test.ts @@ -142,9 +142,9 @@ describe('isAmplifyTriggerPolicyDrift', () => { expect(isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toBe(false); }); - it('throws on malformed JSON', () => { + it('returns false on malformed JSON instead of throwing', () => { const drift = makeDrift({ ResourceType: 'AWS::IAM::Role' }); const propDiff = makePropDiff({ PropertyPath: '/Policies/0', ExpectedValue: 'null', ActualValue: 'not-json' }); - expect(() => isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toThrow(); + expect(isAmplifyTriggerPolicyDrift(drift, propDiff, mockPrinter)).toBe(false); }); }); diff --git a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts index 93a841b2aa6..a6d8cb1513f 100644 --- a/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts +++ b/packages/amplify-cli/src/commands/drift-detection/detect-stack-drift.ts @@ -199,22 +199,27 @@ export function isAmplifyTriggerPolicyDrift(drift: StackResourceDrift, propDiff: // The template has null, the deployed resource has the policy if (propDiff.ExpectedValue !== 'null') return false; - const actualPolicy = JSON.parse(propDiff.ActualValue ?? ''); - const policyName: string = actualPolicy.PolicyName; - const policyDoc = JSON.parse(actualPolicy.PolicyDocument as string); - const actions = new Set(policyDoc.Statement.flatMap((s) => [s.Action].flat())); - - // Auth trigger policies: known Cognito trigger policy pattern - const cognitoActions = ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:GetGroup', 'cognito-idp:CreateGroup']; - const isCognitoTriggerPolicy = policyName === 'AddToGroupCognito' && cognitoActions.every((a) => actions.has(a)); - - // S3 storage trigger policies: known S3 trigger policy pattern - 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; + try { + const actualPolicy = JSON.parse(propDiff.ActualValue ?? ''); + const policyName: string = actualPolicy.PolicyName; + const policyDoc = JSON.parse(actualPolicy.PolicyDocument as string); + const actions = new Set(policyDoc.Statement.flatMap((s) => [s.Action].flat())); + + // Auth trigger policies: known Cognito trigger policy pattern + const cognitoActions = ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:GetGroup', 'cognito-idp:CreateGroup']; + const isCognitoTriggerPolicy = policyName === 'AddToGroupCognito' && cognitoActions.every((a) => actions.has(a)); + + // S3 storage trigger policies: known S3 trigger policy pattern + 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: any) { + print.debug(`Failed to parse trigger policy JSON: ${e.message || 'Unknown error'}`); + return false; } return false;