/** * Drift-suspect confirmation probe (drift-remediation plan § Phase 5 item 2). * Turns SUSPECTS from `classifyDriftFailure.ts` into per-resource verdicts: * * - A `DescribeStacks` existence check first — a deleted stack short-circuits * every suspect to `stack-missing` (it must never read as "nothing wrong"). * - A `ListStackResources` membership gate next — a suspect the stack no * longer tracks (cured by removing it from the template) reads `present` * (recovered), never probed against the owning service; probing its * long-dead physicalId would wrongly confirm and re-journal forever. * - Direct per-type probes (KMS DescribeKey, S3 HeadBucket/GetBucketPolicy, * Secrets Manager DescribeSecret, Route53 GetHostedZone/ * ListResourceRecordSets) — only an authoritative NotFound from the * owning service (or a KMS key in PendingDeletion/PendingReplicaDeletion, * which the CloudFormation handler maps to NotFound) yields * `confirmed-deleted`. * - CloudFormation drift-API fallback (`DetectStackDrift` → poll → * `DescribeStackResourceDrifts`) for types without a direct probe. * * Fail-closed throughout: throttles, AccessDenied, network failures and abort * all yield `unconfirmed` (repair-only presentation downstream), never a * destructive-eligible verdict. * * [G8] boundary audit (2026-07-08; Route53 additions re-audited 2026-07-17): * every action this probe issues — * cloudformation:DescribeStacks/ListStackResources/DetectStackDrift/ * DescribeStackDriftDetectionStatus/DescribeStackResourceDrifts, * kms:DescribeKey, s3:HeadBucket (s3:ListBucket)/GetBucketPolicy, * secretsmanager:DescribeSecret, route53:GetHostedZone/ * ListHostedZonesByName/ListResourceRecordSets — is read-only and appears in * no Deny of the deploy session policy (`deploySessionPolicy.ts` fences * foreign CFN/ECR MUTATION only — enumerated cloudformation and ecr mutation * actions, no wildcards) nor of any security-tier permissions boundary * (`generator/src/schemas/securityBoundary.ts` — every tier's Denies are * enumerated org/cloudtrail/iam/kms/config/ec2 actions, none route53, no * wildcards). No policy change needed. * * Finding `verdictDetail`s are masked at construction, so CLI/journal sinks * may surface them directly. */ import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import { type DriftFinding, type DriftSuspect, type DriftVerdict } from "../../types/drift.js"; export interface ProbeDriftSuspectsParams { stackName: string; suspects: readonly DriftSuspect[]; abortSignal?: AbortSignal; } /** The narrow surface callers need — DriftProbe satisfies it; tests stub. */ export interface DriftProbeRunner { probeSuspects(params: ProbeDriftSuspectsParams): Promise>; } export interface DriftProbeOptions { /** Poll cadence for the drift-API fallback; defaults to 3 s. */ pollIntervalMs?: number; /** Overall drift-API poll budget; defaults to 90 s. */ pollTimeoutMs?: number; /** Injectable clock for timeout tests. */ now?: () => number; } /** * probeMethod stamped by the ListStackResources membership gate below. Its * "present" verdict means "no longer tracked by the stack — cured by removal * from the template", NOT physical presence: no owning service was consulted. * Forget-closure accounting discriminates on this constant to keep * membership-gate verdicts out of the retained-by-redeclaration bucket * (coupled values — shared at 2 occurrences with forgetResource.ts). */ export declare const MEMBERSHIP_GATE_PROBE_METHOD = "cloudformation:ListStackResources"; export interface PhysicalPresenceOutcome { /** `present` (alive), `confirmed-deleted`, or `unconfirmed` — never a * membership-gate verdict; the stack listing is deliberately not consulted. */ verdict: Extract; detail: string; /** The owning-service action consulted, or "none" for unsupported types. */ probeMethod: string; } /** * [G6] Gate-bypassing physical-presence probe: ask the OWNING SERVICE whether * `physicalId` is alive, with no ListStackResources membership gate in front. * `probeSuspects`' gate is correct for drift confirmation (an untracked * suspect is cured) but poisonous at absent-bucket closure — every * absent-from-stack target would read "present"/cured with ZERO physical * verification, so a DELETE_FAILED orphan closed by a later update-cleanup * strip would be recorded as an executed deletion. Types without a direct * probe return `unconfirmed` (probeMethod "none") — callers close hedged. * Fail-soft: SDK failures are `unconfirmed`, never a throw. */ export declare function probePhysicalPresence(aws: AwsProvider, params: { resourceType: string; physicalId: string; abortSignal?: AbortSignal; }): Promise; export declare class DriftProbe implements DriftProbeRunner { private readonly aws; private readonly pollIntervalMs; private readonly pollTimeoutMs; private readonly now; constructor(aws: AwsProvider, options?: DriftProbeOptions); probeSuspects(params: ProbeDriftSuspectsParams): Promise>; private checkStackExists; /** * The stack's current resources: logicalId → PhysicalResourceId ("" when * CloudFormation reports none). Key presence is the membership ground * truth for the cured-by-template-removal gate, so this ALWAYS lists — * suspect-supplied physicalIds (journal-sourced) must never bypass the * membership check. Returns null when the listing fails or aborts * mid-pagination (fail-soft: probing proceeds on suspect-supplied ids and * membership stays unverified rather than misread as "not a member"). */ private listStackResources; /** * Generic fallback for types without a direct probe: run stack drift * detection, then read each suspect's drift status. Only a DELETED drift * status confirms; IN_SYNC/MODIFIED prove presence; everything else * (detection failure, NOT_CHECKED, unsupported type) stays unconfirmed. */ private probeViaDriftApi; }