98 lines
2.6 KiB
JavaScript
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: /sk-(?: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.`);
|