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
111 changes: 104 additions & 7 deletions scripts/ci/validate-content-policy.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node

Check notice on line 1 in scripts/ci/validate-content-policy.mjs

View check run for this annotation

Gittensory / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in scripts/ci/validate-content-policy.mjs

View check run for this annotation

Gittensory / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.

Check notice on line 1 in scripts/ci/validate-content-policy.mjs

View check run for this annotation

Gittensory / Gittensory Context

PR author has maintainer association

This PR appears to come from a maintainer-associated account.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
Expand Down Expand Up @@ -364,6 +364,8 @@
if (url.hostname.toLowerCase() === "raw.githubusercontent.com") {
if (parts.length < 4) return null;
return {
owner: parts[0].toLowerCase(),
repo: parts[1].replace(/\.git$/i, "").toLowerCase(),
ref: parts[2],
path: parts.slice(3).join("/"),
};
Expand All @@ -374,6 +376,8 @@
(parts[2] === "blob" || parts[2] === "raw")
) {
return {
owner: parts[0].toLowerCase(),
repo: parts[1].replace(/\.git$/i, "").toLowerCase(),
ref: parts[3],
path: parts.slice(4).join("/"),
};
Expand All @@ -388,13 +392,106 @@
return /\.(?:sh|bash|zsh|ps1)$/i.test(normalizeText(value));
}

function isImmutableGithubScriptSourceUrl(value) {
const source = githubSourceRef(value);
return Boolean(
source &&
FULL_COMMIT_SHA_PATTERN.test(source.ref) &&
isScriptPath(source.path),
function githubRepoRef(value) {
const raw = normalizeText(value);
if (!raw) return null;
try {
const url = new URL(raw);
if (
url.protocol !== "https:" ||
url.hostname.toLowerCase() !== "github.com"
) {
return null;
}
const parts = url.pathname.split("/").filter(Boolean);
if (parts.length < 2) return null;
return {
owner: parts[0].toLowerCase(),
repo: parts[1].replace(/\.git$/i, "").toLowerCase(),
};
} catch {
return null;
}
}

function githubRepoKey(source) {
if (!source?.owner || !source?.repo) return "";
return `${source.owner}/${source.repo}`;
}

function collectGitCloneRepos(value) {
const text = normalizeText(value);
const repos = [];
const clonePattern =
/\bgit\s+clone\b[^\n;&|]*?(https:\/\/github\.com\/[^\s`'"<>]+|git@github\.com:[^\s`'"<>]+)/gi;
for (const match of text.matchAll(clonePattern)) {
const rawRepo = match[1].replace(
/^git@github\.com:/i,
"https://github.com/",
);
const repo = githubRepoRef(rawRepo);
if (repo) repos.push(repo);
}
return repos;
}

function normalizeScriptPath(value) {
return normalizeText(value)
.replace(/^['"`]+|['"`]+$/g, "")
.replace(/^(?:\.\.?\/)+/, "")
.replace(/\/{2,}/g, "/")
.toLowerCase();
}

function collectLocalScriptInstallRefs(value) {
const text = normalizeText(value);
const scriptPattern =
/(?:^|[\s`;&|])(?:(?:bash|sh|zsh|pwsh|powershell)\s+)?((?:\.{1,2}\/|[\w.-]+\/)?[\w./-]*(?:install|setup|start|bootstrap|init)[\w.-]*\.(?:sh|bash|zsh|ps1))\b/gim;
return [...text.matchAll(scriptPattern)]
.map((match) => ({
path: normalizeScriptPath(match[1]),
index: match.index ?? 0,
}))
.filter((script) => script.path);
}

function checkoutCommitIndex(value, commit) {
const escapedCommit = commit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = normalizeText(value).match(
new RegExp(
`\\bgit\\s+(?:checkout|switch\\s+--detach)\\s+(?:[^\\n;&|]*\\s)?${escapedCommit}\\b`,
"i",
),
);
return match?.index ?? -1;
}

function hasBoundImmutableGithubScriptEvidence(sourceUrls, installText) {
const clonedRepoKeys = new Set(
collectGitCloneRepos(installText).map(githubRepoKey),
);
if (!clonedRepoKeys.size) return false;
const executedScripts = collectLocalScriptInstallRefs(installText);
if (!executedScripts.length) return false;

return sourceUrls.some((value) => {
const source = githubSourceRef(value);
if (
!source ||
!FULL_COMMIT_SHA_PATTERN.test(source.ref) ||
!isScriptPath(source.path) ||
!clonedRepoKeys.has(githubRepoKey(source))
) {
return false;
}

const checkoutIndex = checkoutCommitIndex(installText, source.ref);
if (checkoutIndex < 0) return false;
const scriptPath = normalizeScriptPath(source.path);
return executedScripts.some(
(script) => script.path === scriptPath && checkoutIndex < script.index,
);
});
}

function isMutableGithubSourceUrl(value) {
Expand Down Expand Up @@ -735,7 +832,7 @@

if (
referencesClonedLocalScriptInstall(installText) &&
!submittedSourceUrls.some(isImmutableGithubScriptSourceUrl)
!hasBoundImmutableGithubScriptEvidence(submittedSourceUrls, installText)
) {
const mutableSources = submittedSourceUrls.filter(isMutableGithubSourceUrl);
addFlag(
Expand Down
130 changes: 130 additions & 0 deletions tests/content-policy-validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync } from "node:child_process";

Check notice on line 1 in tests/content-policy-validation.test.ts

View check run for this annotation

Gittensory / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in tests/content-policy-validation.test.ts

View check run for this annotation

Gittensory / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.

Check notice on line 1 in tests/content-policy-validation.test.ts

View check run for this annotation

Gittensory / Gittensory Context

PR author has maintainer association

This PR appears to come from a maintainer-associated account.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -157,6 +157,136 @@
);
});

it("rejects unrelated immutable script evidence for cloned local scripts", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "heyclaude-content-policy-"),
);
const unrelatedRevision = "0123456789abcdef0123456789abcdef01234567";
const content = `---
title: Example MCP
category: mcp
description: Example MCP server with unrelated pinned script evidence.
repoUrl: https://github.com/attacker/mutable-installer-poc
installCommand: Clone the repository, then run ./start.sh.
safetyNotes:
- Runs a local startup script from cloned source.
sourceUrls:
- https://raw.githubusercontent.com/unrelated/benign/${unrelatedRevision}/scripts/safe.sh
---

Clone the repository and start the server:

\`\`\`bash
git clone https://github.com/attacker/mutable-installer-poc.git
cd mutable-installer-poc
./start.sh
\`\`\`
`;

const result = runContentPolicy(tmpDir, content, "same_repo_direct", [
{
filename: "content/mcp/example-mcp.mdx",
status: "added",
content,
},
]);

expect(result.status).not.toBe(0);
const output = JSON.parse(fs.readFileSync(result.outputJson, "utf8"));
expect(output.reviewFlags).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "mutable_script_install_source" }),
]),
);
});

it("rejects immutable evidence for a different script path in the cloned repo", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "heyclaude-content-policy-"),
);
const revision = "9845479d0aeb7523abaab85723d0dfcf832fe1d3";
const content = `---
title: Example MCP
category: mcp
description: Example MCP server with pinned evidence for the wrong script.
repoUrl: https://github.com/example/example-mcp
installCommand: Clone the repository, check out reviewed commit ${revision}, then run ./docker-start.sh.
safetyNotes:
- Runs a local Docker stack from cloned source.
sourceUrls:
- https://raw.githubusercontent.com/example/example-mcp/${revision}/scripts/safe-start.sh
---

Clone the repository and start the stack:

\`\`\`bash
git clone https://github.com/example/example-mcp.git
cd example-mcp
git checkout ${revision}
./docker-start.sh
\`\`\`
`;

const result = runContentPolicy(tmpDir, content, "same_repo_direct", [
{
filename: "content/mcp/example-mcp.mdx",
status: "added",
content,
},
]);

expect(result.status).not.toBe(0);
const output = JSON.parse(fs.readFileSync(result.outputJson, "utf8"));
expect(output.reviewFlags).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "mutable_script_install_source" }),
]),
);
});

it("rejects immutable script evidence unless install instructions check out that commit", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "heyclaude-content-policy-"),
);
const revision = "9845479d0aeb7523abaab85723d0dfcf832fe1d3";
const content = `---
title: Example MCP
category: mcp
description: Example MCP server with a pinned script URL but mutable checkout.
repoUrl: https://github.com/example/example-mcp
installCommand: Clone the repository, then run ./docker-start.sh.
safetyNotes:
- Runs a local Docker stack from cloned source.
sourceUrls:
- https://raw.githubusercontent.com/example/example-mcp/${revision}/docker-start.sh
---

Clone the repository and start the stack:

\`\`\`bash
git clone https://github.com/example/example-mcp.git
cd example-mcp
./docker-start.sh
\`\`\`
`;

const result = runContentPolicy(tmpDir, content, "same_repo_direct", [
{
filename: "content/mcp/example-mcp.mdx",
status: "added",
content,
},
]);

expect(result.status).not.toBe(0);
const output = JSON.parse(fs.readFileSync(result.outputJson, "utf8"));
expect(output.reviewFlags).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "mutable_script_install_source" }),
]),
);
});

it("allows cloned local scripts with immutable script source evidence", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "heyclaude-content-policy-"),
Expand Down
Loading