/** * Shared utility for extracting CloudFormation resource references * from template properties. * * Used by WAW010 (redundant DependsOn) and COR020 (circular deps). */ /** * Parsed CloudFormation template structure. */ export interface CFTemplate { AWSTemplateFormatVersion?: string; Resources?: Record; [key: string]: unknown; } export interface CFResource { Type: string; Properties?: Record; DependsOn?: string | string[]; [key: string]: unknown; } /** * Parse a serialized CloudFormation template from build output. * Accepts either a raw string or a SerializerResult (extracts primary). */ export function parseCFTemplate(output: string | { primary: string }): CFTemplate | null { const raw = typeof output === "string" ? output : output.primary; try { const parsed = JSON.parse(raw); if (typeof parsed === "object" && parsed !== null) { return parsed as CFTemplate; } } catch { // Not valid JSON } return null; } /** * Recursively walk a CloudFormation property value and extract all logical IDs * referenced via Ref and Fn::GetAtt. * * Skips pseudo-parameters (those starting with "AWS::"). */ export function findResourceRefs(value: unknown): Set { const refs = new Set(); walkValue(value, refs); return refs; } /** * Build the reverse of `findResourceRefs` for a whole template: logical id → * the set of logical ids whose Properties reference it via Ref/Fn::GetAtt. * Lets a check walk the graph consumer-ward (who uses this role?) instead of * dependency-ward. Used by WAW059. */ export function buildReverseRefIndex(template: CFTemplate): Map> { const index = new Map>(); for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { for (const target of findResourceRefs(resource.Properties)) { if (target === logicalId) continue; let consumers = index.get(target); if (!consumers) { consumers = new Set(); index.set(target, consumers); } consumers.add(logicalId); } } return index; } /** * Check if a value is a CloudFormation intrinsic function (Ref, Fn::*, etc.) * that cannot be statically evaluated. */ export function isIntrinsic(value: unknown): boolean { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const obj = value as Record; return "Ref" in obj || Object.keys(obj).some((k) => k.startsWith("Fn::")); } /** * Walk IAM policy statements from a resource's properties. * Handles IAM::Policy, IAM::Role, and IAM::ManagedPolicy layouts. */ export function walkPolicyStatements( resource: CFResource, ): Array> { const statements: Array> = []; const props = resource.Properties ?? {}; // PolicyDocument.Statement (IAM::Policy, IAM::ManagedPolicy) collectStatements(props.PolicyDocument, statements); // AssumeRolePolicyDocument.Statement (IAM::Role) collectStatements(props.AssumeRolePolicyDocument, statements); // Policies[].PolicyDocument.Statement (IAM::Role inline policies) if (Array.isArray(props.Policies)) { for (const policy of props.Policies) { if (typeof policy === "object" && policy !== null) { collectStatements((policy as Record).PolicyDocument, statements); } } } return statements; } function collectStatements( policyDoc: unknown, out: Array>, ): void { if (typeof policyDoc !== "object" || policyDoc === null) return; const doc = policyDoc as Record; if (Array.isArray(doc.Statement)) { for (const stmt of doc.Statement) { if (typeof stmt === "object" && stmt !== null) { out.push(stmt as Record); } } } } /** * Normalize security group ingress rules from inline SecurityGroupIngress * property and standalone SecurityGroupIngress resources. */ export function getSecurityGroupIngress( resource: CFResource, ): Array> { const rules: Array> = []; const props = resource.Properties ?? {}; if (Array.isArray(props.SecurityGroupIngress)) { for (const rule of props.SecurityGroupIngress) { if (typeof rule === "object" && rule !== null) { rules.push(rule as Record); } } } return rules; } /** * Extract an ECS TaskDefinition's container definitions (shared by the * WAW046/047/048 ECS checks). */ export function getContainerDefinitions(resource: CFResource): Array> { const props = resource.Properties ?? {}; const defs = props.ContainerDefinitions; if (!Array.isArray(defs)) return []; return defs.filter((d): d is Record => typeof d === "object" && d !== null); } /** * Whether a build's `env` (from `--env` or the project's `ownership.env`, see * PostSynthContext#env and #201) should be treated as the strict "full"/ * production tier for tier-gated checks, vs a relaxed "light" tier acceptable * for local/Floci-style stacks. Reuses the existing `ctx.env` seam — no new * tier field. An undefined or unrecognized env is treated as non-strict so a * project that never sets `--env`/`ownership.env` isn't unexpectedly hard-failed. */ export function isFullTierEnv(env: string | undefined): boolean { return env === "prod" || env === "production" || env === "full"; } /** * Parse an IPv4 CIDR literal ("10.0.0.0/16") into its numeric base address * and prefix length. Returns null for anything that isn't a plain IPv4 CIDR * (IPv6, malformed octets, out-of-range prefix) — callers treat that as * "can't prove statically" rather than an error. */ export function parseIpv4Cidr(cidr: string): { base: number; prefix: number } | null { const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(cidr.trim()); if (!m) return null; const octets = m.slice(1, 5).map(Number); if (octets.some((o) => o < 0 || o > 255)) return null; const prefix = Number(m[5]); if (prefix < 0 || prefix > 32) return null; const base = ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0; return { base, prefix }; } /** [networkAddress, broadcastAddress] for a parsed IPv4 CIDR block. */ function ipv4Range(cidr: { base: number; prefix: number }): [number, number] { const maskBits = cidr.prefix === 0 ? 0 : (0xffffffff << (32 - cidr.prefix)) >>> 0; const network = (cidr.base & maskBits) >>> 0; const broadcast = (network | (~maskBits >>> 0)) >>> 0; return [network, broadcast]; } /** * Whether `inner` (an IPv4 CIDR literal) falls entirely within `outer`. * Returns null — not false — when either literal isn't a plain, parseable * IPv4 CIDR (e.g. IPv6, or a CloudFormation intrinsic already stringified * elsewhere); a null means "statically unprovable", not "violation". */ export function ipv4CidrContains(outer: string, inner: string): boolean | null { const outerCidr = parseIpv4Cidr(outer); const innerCidr = parseIpv4Cidr(inner); if (!outerCidr || !innerCidr) return null; const [oStart, oEnd] = ipv4Range(outerCidr); const [iStart, iEnd] = ipv4Range(innerCidr); return iStart >= oStart && iEnd <= oEnd; } /** * Check if a port range [fromPort, toPort] contains any of the sensitive ports. */ export function portRangeContainsSensitive( fromPort: unknown, toPort: unknown, sensitivePorts: number[], ): boolean { // Missing ports means all ports if (fromPort === undefined && toPort === undefined) return true; const from = typeof fromPort === "number" ? fromPort : -1; const to = typeof toPort === "number" ? toPort : -1; // If either is an intrinsic, we can't statically verify if (isIntrinsic(fromPort) || isIntrinsic(toPort)) return false; if (from === -1 && to === -1) return true; for (const port of sensitivePorts) { if (from <= port && port <= to) return true; } return false; } function walkValue(value: unknown, refs: Set): void { if (value === null || value === undefined) return; if (typeof value !== "object") return; if (Array.isArray(value)) { for (const item of value) { walkValue(item, refs); } return; } const obj = value as Record; // Check for Ref if ("Ref" in obj && typeof obj.Ref === "string") { if (!obj.Ref.startsWith("AWS::")) { refs.add(obj.Ref); } } // Check for Fn::GetAtt if ("Fn::GetAtt" in obj) { const getAtt = obj["Fn::GetAtt"]; if (Array.isArray(getAtt) && getAtt.length >= 1 && typeof getAtt[0] === "string") { refs.add(getAtt[0]); } else if (typeof getAtt === "string") { // Dot-delimited form: "LogicalId.Attribute" const logicalId = getAtt.split(".")[0]; if (logicalId) refs.add(logicalId); } } // Recurse into all object values (including intrinsic function arguments) for (const val of Object.values(obj)) { if (typeof val === "object" && val !== null) { walkValue(val, refs); } } }