ai-chatbot/scripts/check-secrets.mjs

98 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { extname } from "node:path";
const skippedExtensions = new Set([
".avif",
".bin",
".br",
".gif",
".ico",
".jpg",
".jpeg",
".lock",
".mp3",
".mp4",
".onnx",
".pdf",
".png",
".tar",
".tgz",
".webp",
".woff",
".woff2",
".zip",
]);
const skippedFiles = new Set(["pnpm-lock.yaml"]);
const secretPatterns = [
{ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{24,}/, reason: "OpenAI-style API key" },
{ pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
{ pattern: /AKIA[0-9A-Z]{16}/, reason: "AWS access key id" },
{ pattern: /xox[baprs]-[A-Za-z0-9-]{24,}/, reason: "Slack token" },
{ pattern: /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/, reason: "private key block" },
{ pattern: /\bAuthorization\s*[:=]\s*Bearer\s+[A-Za-z0-9._-]{24,}/i, reason: "Authorization bearer token" },
{ pattern: /\bCookie\s*[:=]\s*[^"'`\s]{40,}/i, reason: "Cookie header value" },
];
const allowedExampleLine = (line) => {
const normalized = line.toLowerCase();
return (
normalized.includes("xxxx") ||
normalized.includes("placeholder") ||
normalized.includes("replace-with") ||
normalized.includes("change-me") ||
normalized.includes("example") ||
normalized.includes("dummy") ||
normalized.includes("redaction") ||
normalized.includes("masked") ||
normalized.includes("begin private key-----\"") ||
normalized.includes("begin private key-----'")
);
};
const trackedFiles = execFileSync("git", ["ls-files"], { encoding: "utf8" })
.split(/\r?\n/)
.filter(Boolean)
.filter((file) => !skippedFiles.has(file))
.filter((file) => !skippedExtensions.has(extname(file).toLowerCase()));
const violations = [];
for (const file of trackedFiles) {
let source;
try {
source = readFileSync(file, "utf8");
} catch {
continue;
}
if (source.includes("\u0000")) {
continue;
}
const lines = source.split(/\r?\n/);
lines.forEach((line, index) => {
if (allowedExampleLine(line)) {
return;
}
for (const { pattern, reason } of secretPatterns) {
if (pattern.test(line)) {
violations.push(`${file}:${index + 1}: ${reason}: ${line.trim()}`);
}
}
});
}
if (violations.length > 0) {
console.error("Secret hygiene check failed. Remove real credentials or replace them with placeholders.");
console.error(violations.join("\n"));
process.exit(1);
}
console.log(`Secret hygiene check passed for ${trackedFiles.length} tracked text files.`);