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
175 changes: 175 additions & 0 deletions scripts/publication-safety.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkPublication, trackedPublicationIssues } from "./check-publication.ts";
import { assertPublicMessage, publicationIssues } from "./publication-safety.ts";

test("synthetic private content is refused without echoing its value", () => {
for (const value of [
"/" + "Users/fictional/project",
"github_" + "pat_" + "SYNTHETIC".repeat(8),
"FixtureReviewer" + " 9.9 Max review",
"# Session" + " report",
"Includes 12" + " merged Engine pull requests",
]) {
expect(publicationIssues(value).length).toBeGreaterThan(0);
try {
assertPublicMessage({ output: { summary: value } });
throw new Error("Expected rejection");
} catch (error) {
expect(String(error)).toContain("Public message refused:");
expect(String(error)).not.toContain(value);
}
}
});

test("public AI, agent and spec terminology and technical provenance remain valid", () => {
expect(() =>
assertPublicMessage([
"AI integration uses a documented API.",
"The agent runs validation; this spec defines the public schema.",
{ source_repository: "rootform-dev/engine", source_commit: "a".repeat(40) },
"The binary handoff verifies exact provenance.",
]),
).not.toThrow();
});

test("serialized and URL-encoded metadata is inspected before sending", () => {
expect(() => assertPublicMessage(undefined)).not.toThrow();
const path = "/" + "Users/fictional/session";
const token = "github_" + "pat_" + "SYNTHETIC".repeat(8);
for (const value of [path, token]) {
for (const serialized of [
encodeURIComponent(value),
encodeURIComponent(encodeURIComponent(value)),
JSON.stringify({ value }).replaceAll("/", "\\/"),
JSON.stringify({ value }).replaceAll("/", "\\u002f"),
{ encoding: "base64", content: btoa(value) },
{ encoding: "base64", content: btoa(JSON.stringify({ value }).replaceAll("/", "\\u002f")) },
JSON.stringify({ API_TOKEN: value }),
{ message: "Update public fixture", content: btoa(value) },
]) {
try {
assertPublicMessage(serialized);
throw new Error("Expected rejection");
} catch (error) {
expect(String(error)).toContain("Public message refused:");
expect(String(error)).not.toContain(value);
}
}
}
expect(() =>
assertPublicMessage({ encoding: "base64", content: btoa("Public fixture") }),
).not.toThrow();
expect(() => assertPublicMessage({ encoding: "base64", content: "?" })).toThrow(
"uninspectable-content",
);
});

test("nested JSON and rendered metadata cannot hide private text", () => {
const credential = "SYNTHETIC".repeat(8);
const path = "/" + "Users/fictional/session";
for (const value of [
{ body: JSON.stringify({ API_KEY: credential }) },
'"api_key": "' + credential + '"',
{ body: JSON.stringify({ CLOUDFLARE_API_KEY: credential }) },
{ encoding: "base64", content: Buffer.from(path, "utf16le").toString("base64") },
["&#47", ";Users&#47", ";fictional&#47", ";session"].join(""),
"notes/" + "fictional-session/report.json",
])
expect(() => assertPublicMessage(value)).toThrow("Public message refused:");
for (const value of [
"ROOTFORM_DATADOG_CLOUDFLARE_KEY_SENTINEL",
"ROOTFORM_DATADOG_FASTLY_KEY_SENTINEL",
"ROOTFORM_HCP_DATADOG_API_SENTINEL",
"ROOTFORM_ATLAS_OBSERVABILITY_SECRET",
])
expect(() => assertPublicMessage('api_key = "' + value + '"')).not.toThrow();
});

test("tracked ignored files and staged bytes cannot escape the publication scan", () => {
const directory = mkdtempSync(join(tmpdir(), "publication-fixture-"));
const git = (...args: string[]) => execFileSync("git", args, { cwd: directory });
try {
git("init", "-q");
writeFileSync(join(directory, ".gitignore"), "artifacts/\n");
mkdirSync(join(directory, "artifacts"));
writeFileSync(join(directory, "artifacts/tracked.txt"), "/" + "Users/fictional/hidden");
git("add", ".gitignore");
git("add", "-f", "artifacts/tracked.txt");
expect(trackedPublicationIssues(directory)[0]?.rule).toBe("personal-path");
writeFileSync(join(directory, "artifacts/tracked.txt"), "Clean working copy.\n");
expect(() => checkPublication(["--repo", directory, "--staged"])).toThrow();
} finally {
rmSync(directory, { recursive: true, force: true });
}
});

test("large staged files are inspected and Git failures never echo their input", () => {
const directory = mkdtempSync(join(tmpdir(), "publication-large-fixture-"));
const git = (...args: string[]) => execFileSync("git", args, { cwd: directory });
try {
git("init", "-q");
writeFileSync(
join(directory, "bundle.js"),
`${"x".repeat(2 * 1024 * 1024)}\n/${"Users/fictional/hidden"}`,
);
git("add", "bundle.js");
expect(trackedPublicationIssues(directory, ":")[0]?.rule).toBe("personal-path");
const revision = "/" + "Users/fictional/invalid";
expect(() => trackedPublicationIssues(directory, revision)).toThrow(
"cannot read tracked Git content",
);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});

test("staged symlink modes cannot be concealed by the working copy", () => {
const directory = mkdtempSync(join(tmpdir(), "publication-link-fixture-"));
const git = (...args: string[]) => execFileSync("git", args, { cwd: directory });
try {
git("init", "-q");
symlinkSync("nonexistent-fixture", join(directory, "alias"));
git("add", "alias");
rmSync(join(directory, "alias"));
writeFileSync(join(directory, "alias"), "Regular working copy.\n");
expect(trackedPublicationIssues(directory)).toEqual([]);
expect(trackedPublicationIssues(directory, ":")[0]?.rule).toBe("symlink");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});

test("named credentials are checked recursively while exact synthetic literals remain valid", () => {
const credential = "SYNTHETIC".repeat(8);
for (const payload of [
{ API_TOKEN: credential },
{ encoding: "base64", content: btoa(JSON.stringify({ api_key: credential })) },
{
encoding: "base64",
content: btoa(
JSON.stringify({ encoding: "base64", content: btoa("github_" + "pat_" + credential) }),
),
},
])
expect(() => assertPublicMessage(payload)).toThrow("credential");
for (const value of [
"ROOTFORM_DATADOG_CLOUDFLARE_KEY_SENTINEL",
"ROOTFORM_DATADOG_FASTLY_KEY_SENTINEL",
"ROOTFORM_HCP_DATADOG_API_SENTINEL",
"ROOTFORM_ATLAS_OBSERVABILITY_SECRET",
]) {
expect(() => assertPublicMessage({ api_key: value })).not.toThrow();
expect(() => assertPublicMessage({ api_key: value + "_CHANGED" })).toThrow("credential");
}
const path = "/" + "Users/fictional/session";
expect(() =>
assertPublicMessage({
encoding: "base64",
content: btoa(JSON.stringify({ value: path }).replaceAll("/", "\\u002f")),
}),
).toThrow("personal-path");
});
149 changes: 135 additions & 14 deletions scripts/publication-safety.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
export type PublicationIssue = { rule: string; line: number };

// Exact non-credential literals used by public synthetic provider fixtures.
const syntheticCredentialValues = new Set([
"fixture000000000000000000000000000000000",
"ROOTFORM_DATADOG_PROVIDER_API_KEY_SENTINEL",
"ROOTFORM_NEWRELIC_BOUNDARY_SECRET_SENTINEL",
"ROOTFORM_NEWRELIC_PROVIDER_KEY_SENTINEL",
"ROOTFORM_OKTA_BOUNDARY_SECRET_SENTINEL",
"ROOTFORM_OKTA_PROVIDER_TOKEN_SENTINEL",
"ROOTFORM_DATADOG_CLOUDFLARE_KEY_SENTINEL",
"ROOTFORM_DATADOG_FASTLY_KEY_SENTINEL",
"ROOTFORM_HCP_DATADOG_API_SENTINEL",
"ROOTFORM_ATLAS_OBSERVABILITY_SECRET",
]);

const rules: Array<[string, RegExp]> = [
[
"personal-path",
/\/Users\/[^\s"'<>]+|\/home\/(?!rootform(?:\/|$)|runner(?:\/|$))[A-Za-z0-9._-]+\/|[A-Za-z]:\\Users\\/gu,
],
[
"credential",
/BEGIN (?:RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|(?:github_pat_|gh[pousr]_|npm_)[A-Za-z0-9_]{12,}|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|xox[abprs]-[A-Za-z0-9-]{12,}|AIza[A-Za-z0-9_-]{35}|\bBearer\s+[A-Za-z0-9._~+/-]{24,}=*|\b(?:CLOUDFLARE_API_TOKEN|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)\s*[:=]\s*["\x27]?[A-Za-z0-9_+./~-]{24,}/gu,
/BEGIN (?:RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|(?:github_pat_|gh[pousr]_|npm_)[A-Za-z0-9_]{12,}|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|xox[abprs]-[A-Za-z0-9-]{12,}|AIza[A-Za-z0-9_-]{35}|\bBearer\s+[A-Za-z0-9._~+/-]{24,}=*/gu,
],
["private-workspace", /\b(?:rootform-dev\/)?notes\/[A-Za-z0-9][A-Za-z0-9._/-]*/gu],
[
Expand All @@ -22,22 +36,129 @@ const rules: Array<[string, RegExp]> = [

/** Return only rule and line; never retain or display the matched value. */
export function publicationIssues(text: string): PublicationIssue[] {
return rules.flatMap(([rule, pattern]) =>
[...text.matchAll(pattern)].map((match) => ({
rule,
line: text.slice(0, match.index).split("\n").length,
})),
);
const found: PublicationIssue[] = [];
const scan = (source: string) => {
for (let depth = 0; depth < 3; depth++) {
const decoded = source
.replace(/(?:%[0-9a-f]{2})+/giu, (encoded) => {
try {
return decodeURIComponent(encoded);
} catch {
return encoded;
}
})
.replace(
/&(?:#(x[0-9a-f]+|[0-9]+)|amp|sol|colon|quot|apos);/giu,
(entity, numeric: string | undefined) => {
if (numeric) {
const code = numeric.toLowerCase().startsWith("x")
? Number.parseInt(numeric.slice(1), 16)
: Number(numeric);
return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity;
}
return (
(
{
"&amp;": "&",
"&sol;": "/",
"&colon;": ":",
"&quot;": '"',
"&apos;": "'",
} as Record<string, string>
)[entity.toLowerCase()] ?? entity
);
},
)
.replaceAll("\0", "");
if (decoded === source) break;
source = decoded;
}
for (const [rule, pattern] of rules)
for (const match of source.matchAll(pattern))
found.push({ rule, line: source.slice(0, match.index).split("\n").length });
const namedCredential =
/\b(?:CLOUDFLARE_API_(?:TOKEN|KEY)|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)["']?\s*[:=]\s*["']?([A-Za-z0-9_+./~=-]{24,})/giu;
for (const match of source.matchAll(namedCredential))
if (!syntheticCredentialValues.has(match[1] ?? ""))
found.push({ rule: "credential", line: source.slice(0, match.index).split("\n").length });
};
const inspect = (value: unknown, depth: number) => {
if (depth > 128) {
found.push({ rule: "uninspectable-content", line: 1 });
return;
}
if (typeof value === "string") {
scan(value);
if (/^\s*[[{]/u.test(value)) {
let nested: unknown;
try {
nested = JSON.parse(value);
} catch {
return;
}
inspect(nested, depth + 1);
}
} else if (Array.isArray(value)) for (const item of value) inspect(item, depth + 1);
else if (value !== null && typeof value === "object") {
const payload = value as Record<string, unknown>;
if (
typeof payload.content === "string" &&
(payload.encoding === "base64" || ("message" in payload && payload.encoding === undefined))
) {
try {
if (
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
payload.content,
)
)
throw new Error("Invalid encoding");
const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(
Uint8Array.from(atob(payload.content), (character) => character.charCodeAt(0)),
);
inspect(decoded, depth + 1);
} catch {
found.push({ rule: "uninspectable-content", line: 1 });
}
}
for (const [key, item] of Object.entries(payload)) {
if (
/^(?:CLOUDFLARE_API_(?:TOKEN|KEY)|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)$/iu.test(
key,
) &&
typeof item === "string" &&
!syntheticCredentialValues.has(item) &&
/^[A-Za-z0-9_+./~=-]{24,}$/u.test(item)
)
found.push({ rule: "credential", line: 1 });
scan(key);
inspect(item, depth + 1);
}
}
};
scan(text);
// Parsing valid JSON reveals escapes without interpreting regex or source-code literals.
let structured: unknown;
try {
structured = JSON.parse(text);
} catch {
return found;
}
inspect(structured, 0);
return found;
}

/** Validate all outbound strings before a bot makes its first write. */
export function assertPublicMessage(value: unknown): void {
if (typeof value === "string") {
const issue = publicationIssues(value)[0];
if (issue) throw new Error(`Public message refused: ${issue.rule}`);
} else if (Array.isArray(value)) {
for (const item of value) assertPublicMessage(item);
} else if (value !== null && typeof value === "object") {
for (const item of Object.values(value)) assertPublicMessage(item);
if (value === undefined) return;
let text: string;
try {
const serialized = typeof value === "string" ? value : JSON.stringify(value);
if (typeof serialized !== "string") throw new Error("Invalid payload");
text = serialized;
} catch {
throw new Error("Public message refused: uninspectable-content");
}
const issues = publicationIssues(text);
const issue = issues[0];
if (issue) throw new Error(`Public message refused: ${issue.rule}`);
}
1 change: 1 addition & 0 deletions scripts/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ try {

for (const command of [
["bun", "scripts/check-publication.ts"],
["bun", "test", "scripts/publication-safety.test.ts"],
["gitleaks", "git", "--no-banner", "--redact", "--config", ".gitleaks.toml", "."],
["bun", "run", "check:format"],
["bun", "run", "build:vscode"],
Expand Down
Loading
Loading