/** * infra-identifiers.ts — R4 enforcement: vendor infrastructure identifiers live * behind ONE indirection. * * The open-source repository must never name the infrastructure of whoever * happens to operate it. Account IDs, ARNs, ECS cluster/service/task-family * names, container names and health hosts are properties of a *deployment*, not * of the software, so they resolve at deploy time from a deploy manifest or CI * variables. At most one tracked line may name where that manifest lives. * * This module expresses that as a set of PROPERTIES rather than a blocklist of * known-bad strings, so it survives renaming (`skills-prod-worker` -> * `widgets-stage-queue` is caught just the same) and catches semantic inversion * (re-hardcoding a value that used to be parameterized). * * The five properties: * * 1. `aws-account-id` — a literal 12-digit AWS account ID. Anchored on * non-alphanumeric/non-hyphen boundaries so it does NOT fire inside UUIDs * (`...-8000-000000000000`) or 13-digit millisecond epochs. * 2. `aws-arn` — an ARN whose account field is a literal. * 3. `infra-resource-name` — the `-[-]` naming convention * that AWS resources follow, with a LITERAL app segment. A name assembled * from a substitution (`${{ env.APP }}-prod-gha-deploy`) is compliant and * is not flagged. * 4. `unparameterized-workflow-infra` — inside a workflow, an env assignment * whose key names an infrastructure resource (CLUSTER / SERVICE / FAMILY / * CONTAINER / ECR / HEALTH_URL / SUBNET / SECURITY_GROUP) but whose value * contains no substitution at all. This is the rule that catches a plain * literal like `skills-worker` that follows no naming convention. * 5. `workflow-vendor-host` — a literal URL host inside a workflow that is not * a well-known CI/tooling host. Deploy and health targets are deployment * configuration, not source. * * Plus one cardinality property: `manifest-location-not-unique` — more than one * tracked line naming the deploy-manifest location. */ export type InfraRuleId = "aws-account-id" | "aws-arn" | "infra-resource-name" | "unparameterized-workflow-infra" | "workflow-vendor-host" | "manifest-location-not-unique" | "unscannable-file" | "unparseable-workflow"; export type InfraFinding = { file: string; line: number; ruleId: InfraRuleId; /** The offending text, trimmed to keep CI logs readable. */ match: string; message: string; }; export type ScannedFile = { /** Repo-relative path, POSIX separators. */ path: string; content: string; }; /** * The detector's own module and test are exempt from the repository scan. * * They exist to DEFINE and EXERCISE these patterns, so they necessarily contain * a literal account ID, a literal ARN and a literal resource name as test * vectors — a scanner that flags its own fixtures is a scanner that gets * disabled. The exemption is deliberately two exact paths, never a prefix or a * glob, and `infra-identifiers.test.ts` asserts that this set does not grow. * (Same shape as the audited `scanAllowlist` in scripts/release-guard.ts.) */ export declare const SELF_EXCLUDED_PATHS: readonly string[]; /** * Scan already-loaded file contents for R4 violations. Pure — no filesystem, no * git — so it is trivially testable with synthetic inputs. */ export declare function scanInfraIdentifiers(files: ScannedFile[]): InfraFinding[]; /** * List git-tracked files. `git ls-files` never descends into `node_modules` * unless it is tracked, which sidesteps the CI-grep-vs-interactive-grep trap: * GNU grep in CI does not auto-exclude `node_modules`, and two vendored trees * exist in this repo. The explicit filter below is belt-and-braces for the case * where a vendored tree IS tracked. */ /** * Whether `repoRoot` is inside a git work tree. * * R4 is a property of a *repository*: the leak it prevents lives in tracked * files such as `.github/workflows`. The release guard is also exercised against * synthetic package fixtures in a bare temp directory, where there are no * tracked files and the rule has nothing to say. Callers use this to tell * "not applicable here" apart from "the scan broke", which must stay fatal. * * This is NOT an escape hatch for the real repository: `bun test` runs the scan * against the repo root on every CI run regardless of what the release guard * decides, so the property cannot lapse by moving where the guard is invoked. */ export declare function isGitWorkTree(repoRoot: string): boolean; export declare function listTrackedFiles(repoRoot: string): string[]; /** * Why a tracked file was not scanned. Every tracked path lands in exactly one of * scanned / skipped / self-excluded, and the scan asserts the three add up. * There is no fourth outcome and no silent drop. */ export type SkipReason = "binary-content" | "not-a-regular-file" | "unreadable"; /** * Tracked paths permitted to go unscanned, matched exactly on (path, reason). * * Empty, and asserted empty. Any skip at all fails the scan: a file the guard * did not read is a file the guard did not clear, and "logged but forgiven" is * how the NUL-drop survived in the first place. Adding an entry here is a * deliberate, reviewable act. */ export declare const PINNED_SKIPS: readonly SkippedFile[]; export type SkippedFile = { path: string; reason: SkipReason; }; export type ReadResult = { files: ScannedFile[]; skipped: SkippedFile[]; }; /** * Read tracked files for scanning. * * This function used to do `if (buffer.includes(0)) continue` — dropping any * file containing a NUL byte, with no counter and no warning. That is a bypass, * not a heuristic: appending one NUL to a comment removed a file from the scan * entirely, and `src/lib/content-scan.ts` (which then wrote its composite-key * separator as a raw NUL) was already being dropped on a clean tree. * * A NUL byte is now simply a byte. The content is decoded and scanned like any * other; the patterns work fine on a string that happens to contain U+0000. * The only remaining non-scans are the ones named in SkipReason, and each is * returned to the caller rather than swallowed. */ export declare function readTrackedFiles(repoRoot: string, paths: string[]): ReadResult; export type InfraScanResult = { findings: InfraFinding[]; scannedFileCount: number; /** Every tracked file that was not scanned, with the reason. Never empty-by-omission. */ skippedFiles: SkippedFile[]; trackedFileCount: number; }; /** * Scan the repository's tracked files. * * Throws rather than returning a clean result when the scan cannot honestly * claim coverage: no tracked files, an accounting shortfall, no workflow * scanned, or the deploy pipeline missing from the files actually read. Every * one of those would otherwise be a vacuous pass. */ export declare function scanRepositoryInfraIdentifiers(repoRoot: string): InfraScanResult; export declare function formatInfraFindings(findings: InfraFinding[]): string;