SPB Git

Repository Public

secrets-scan.ts 2.7 KB 54 lines typescript

1import { execFileSync } from "node:child_process";2import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";3import path from "node:path";45/**6 * Fails when a secret could reach a browser or the repository:7 *  1. no xAI key pattern and no value of a secret env var in the built client chunks (.next/static);8 *  2. no key pattern in any git-tracked file;9 *  3. .env is not tracked.10 */11const KEY = /xai-[A-Za-z0-9]{40,}/;12const root = process.cwd();13const problems: string[] = [];1415function walk(dir: string, out: string[] = []): string[] {16  if (!existsSync(dir)) return out;17  for (const name of readdirSync(dir)) {18    const p = path.join(dir, name);19    if (statSync(p).isDirectory()) walk(p, out); else out.push(p);20  }21  return out;22}2324// Secret values present in the environment (never printed).25const secretValues = ["XAI_API_KEY", "APP_SECRET", "DATABASE_URL"].map((k) => {26  if (process.env[k]) return process.env[k]!;27  const env = existsSync(path.join(root, ".env")) ? readFileSync(path.join(root, ".env"), "utf8") : "";28  return env.match(new RegExp(`^${k}=(.*)$`, "m"))?.[1]?.trim() ?? "";29}).filter((v) => v.length >= 16);3031const staticDir = path.join(root, ".next/static");32const chunks = walk(staticDir).filter((f) => /\.(js|css|map|json|html)$/.test(f));33if (!chunks.length) problems.push(".next/static is empty: run the build before the scan");34for (const f of chunks) {35  const text = readFileSync(f, "utf8");36  if (KEY.test(text)) problems.push(`API key pattern in client bundle: ${path.relative(root, f)}`);37  for (const v of secretValues) if (text.includes(v)) problems.push(`secret env value in client bundle: ${path.relative(root, f)}`);38  if (/XAI_API_KEY|APP_SECRET|DATABASE_URL/.test(text)) problems.push(`secret env name referenced in client bundle: ${path.relative(root, f)}`);39}4041let tracked: string[] = [];42try { tracked = execFileSync("git", ["ls-files"], { cwd: root, encoding: "utf8" }).split("\n").filter(Boolean); } catch { /* not a git checkout */ }43for (const f of tracked) {44  if (/^\.env(\..+)?$/.test(f) && f !== ".env.example") problems.push(`environment file is tracked by git: ${f}`);45  const p = path.join(root, f);46  if (!existsSync(p) || statSync(p).size > 2_000_000) continue;47  const text = readFileSync(p, "utf8");48  if (KEY.test(text)) problems.push(`API key pattern in tracked file: ${f}`);49  for (const v of secretValues) if (text.includes(v)) problems.push(`secret env value in tracked file: ${f}`);50}5152if (problems.length) { console.error(`secrets scan FAILED\n${[...new Set(problems)].map((p) => ` - ${p}`).join("\n")}`); process.exit(1); }53console.log(`secrets scan ok: ${chunks.length} client files, ${tracked.length} tracked files, ${secretValues.length} secret values checked`);54
Files main
Menu

Theme