From 16018b53784e0ebfdc00f241cb1b9cf1f8b9268a Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sun, 2 Aug 2026 21:18:47 +0300 Subject: [PATCH 1/2] fix(security): print the file census when the guard FAILS, and pair the arity regression with controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #339. The single-component quantifier fix landed there; these are the two pieces of that review's remediation that did not. 1. The file census moves onto the FAIL path as well as the pass path. The 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 (~1,220 files) to the whole text tree (~22,745). A green step therefore cannot distinguish the narrow guard from the wide one — it proves a guard ran, never which guard ran. The count of files opened is the only observable that changed, which makes it the sole discriminator available to anyone verifying the widened guard is live, and it is now relied on outside this repository as a release-gate corroborator. Printing it only on success meant it vanished exactly when an operator is reading a red log and most needs to know what was scanned. 2. The arity regression gains matched negative controls and an end-to-end arm. #339 asserts the single-component positive. That is necessary and it cannot detect the way this fix would most plausibly regress: closing the arity gap required loosening the name shape, and the loosened shape without its second signal decays into "any hyphenated name containing an environment-looking segment" — measured at 67 matches across 30 files, overwhelmingly false. Every positive still passes under that decay, so positives alone are blind to it. Each arity is now asserted against a control that is the same line with only the resource-kind word swapped, so the pair fails if either the arity fix or the two-signal design regresses. A second arm drives the single-component case through scanPaths() rather than the matcher alone, covering selection, read and rule — the bypass was reachable only because all three lined up, so testing the last step alone would not have caught it. Verified in both directions rather than asserted. Reverting the quantifier fails 3 tests; removing the conjunction fails 2; the paired test is the one that fires on both. Guard on a clean tree exits 0 with "22745 tracked + packed file(s) scanned"; with a synthetic single-component probe it exits 1 and the same census now appears on that line too. Suite 3815 pass / 190 fail against a 3813 / 190 baseline on this commit — identical failure sets, the +2 are these tests. Typecheck clean. No real resource identifier appears in any fixture or message; sentinels are synthetic and were checked against both the naming standard's own example names and the identifiers scrubbed from this connector. Refs: todos 38d15243 Agent: Silvanus --- scripts/check-package-secrets.test.ts | 45 +++++++++++++++++++++++++++ scripts/check-package-secrets.ts | 22 ++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) 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}`); } From e812881496a84082a180af4d7247a2a1a7964dd8 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sun, 2 Aug 2026 21:55:05 +0300 Subject: [PATCH 2/2] fix: make test gate self-contained Agent: unresolved-account002 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"