Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
45 changes: 45 additions & 0 deletions scripts/check-package-secrets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<workload>-<env>-<component>[-<role>]`
// — 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.
Expand Down
22 changes: 18 additions & 4 deletions scripts/check-package-secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
Loading