diff --git a/package.json b/package.json index a323e9d0..b7a7babe 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dev": "bun run ./src/cli/index.tsx", "check:package-secrets": "bun run scripts/check-package-secrets.ts", "typecheck": "tsc --noEmit && tsc -p tsconfig.social.json --noEmit", - "test": "bun test", + "test": "bun run build && bun test", "secrets:scan:npmrc": "bun run scripts/scan-npmrc-auth.mjs", "sdk:build": "cd sdk && bun run build", "prepublishOnly": "bun run secrets:scan:npmrc && bun run check:package-secrets && bun run build && bun test" diff --git a/scripts/check-package-secrets.test.ts b/scripts/check-package-secrets.test.ts index f2fc2bbf..2e6b6791 100644 --- a/scripts/check-package-secrets.test.ts +++ b/scripts/check-package-secrets.test.ts @@ -50,6 +50,51 @@ describe("deployment-identifier rules — the guard must be able to FIRE", () => expect(findings.map((f) => f.rule)).toEqual(["deployment-resource-name"]); }); + // BOTH ARITIES, each against a negative control that differs ONLY in the + // resource-kind word. + // + // The house standard writes the pattern as `--[-]` + // — the role segment is bracketed, so ONE component is the documented default + // and two is the variant. An earlier revision of this rule required two or + // more, which exempted the default; measured against the standard's own worked + // examples, 9 of its 18 distinct convention-shaped names were single-component + // and none of them matched. + // + // The paired negative controls are the load-bearing half. Fixing the arity gap + // meant loosening the name shape, and the obvious way to get that wrong is to + // let the rule decay into "any hyphenated name containing an environment-looking + // segment" — measured at 67 matches across 30 files, overwhelmingly false, and + // unusable. Asserting the positive alone cannot catch that decay, because a + // rule with no second signal still passes every positive. Each control below is + // the same line with only the resource-kind word swapped out, so the pair fails + // if either the arity fix or the two-signal design regresses. + test("fires on BOTH arities, and stays silent when only the kind word is removed", () => { + const row = (kind: string, name: string) => `| ${kind} | \`${name}\` |`; + + for (const name of [SENTINEL_SINGLE_COMPONENT, SENTINEL_NAME]) { + expect(scanDeploymentIdentifiers("x.md", [row("S3 Bucket", name)]).map((f) => f.rule)).toEqual([ + "deployment-resource-name", + ]); + expect(scanDeploymentIdentifiers("x.md", [row("Label", name)])).toEqual([]); + } + }); + + test("flags the single-component form end to end, through file selection", () => { + // Through scanPaths rather than the matcher alone, so the regression covers + // the whole path a real publish would take — selection, read, then rule — + // rather than only the last step of it. The bypass this closes was reachable + // precisely because all three had to line up. + const { paths } = makeTree({ + "README.md": `| EC2 Instance | \`${SENTINEL_SINGLE_COMPONENT}\` |`, + "Makefile": `EC2_HOST ?= ${SENTINEL_SINGLE_COMPONENT}`, + }); + + const { findings, scanned } = scanPaths(paths); + + expect(scanned).toBe(2); + expect(findings.map((f) => f.rule)).toEqual(["deployment-resource-name", "deployment-resource-name"]); + }); + test("flags the naming TEMPLATE, not only concrete names", () => { // Publishing the pattern makes every sibling resource's name derivable, so // scrubbing concrete names alone would leave the disclosure intact. diff --git a/scripts/check-package-secrets.ts b/scripts/check-package-secrets.ts index 4ef79bf1..4a8a7313 100644 --- a/scripts/check-package-secrets.ts +++ b/scripts/check-package-secrets.ts @@ -378,14 +378,28 @@ const { findings, scanned, symlinks, absent } = scanPaths(filesToScan()); const symlinkNote = symlinks > 0 ? `, ${symlinks} symlink(s) skipped` : ""; const absentNote = absent > 0 ? `, ${absent} listed-but-absent path(s) skipped` : ""; +// The file census is emitted on BOTH the pass and the fail path, and that is +// deliberate rather than tidiness. +// +// This script's CI step is named "Package-manager secret guard", and that name +// read identically before and after the scan was widened from a basename +// allow-list to the whole text tree. So a green step on a tree that still +// carries an exposure is indistinguishable from a green step on a clean one — +// the step name is not evidence about which guard ran. The only observable that +// changed is how many files were opened: roughly 1,220 under the old +// basename gate against roughly 22,745 now. That number is therefore the sole +// discriminator available to anyone verifying that the widened guard actually +// ran, and it is relied on outside this repository as a release-gate +// corroborator. A discriminator that disappears exactly when the guard fails is +// no use to an operator reading a red log, so it is printed either way. +const census = `${scanned} tracked + packed file(s) scanned${symlinkNote}${absentNote}`; + if (findings.length === 0) { - console.log( - `Package-manager and deployment-identifier guard clean (${scanned} tracked + packed file(s) scanned${symlinkNote}${absentNote}).`, - ); + console.log(`Package-manager and deployment-identifier guard clean (${census}).`); process.exit(0); } -console.error(`${findings.length} package-manager / deployment-identifier finding(s) detected.`); +console.error(`${findings.length} package-manager / deployment-identifier finding(s) detected (${census}).`); for (const finding of findings.slice(0, 50)) { console.error(`${finding.path}:${finding.line} ${finding.rule} - ${finding.detail}`); }