#!/usr/bin/env node import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; export const DEFAULT_SCAN_FILES = [ "AGENTS.md", "README.md", "server/README.md", "client/README.md", "server/.env.example", "docker-compose.yml", "deploy/trialrun/deploy.sh", "deploy/trialrun/.env.example", "deploy/trialrun/runbook.md", "deploy/trialrun/checklist.md", "deploy/trialrun/setup-server.sh", "docs/Agent开发交付实践.md", "docs/文档索引.md", "docs/文档事实源状态矩阵.md", "docs/产品设计规则.md", "docs/报销发票附件规则.md", "docs/试运行与自动化验证手册.md", ]; const CURRENT_DOCUMENT_REGISTRY = "docs/文档事实源状态矩阵.md"; const TEXT_PATTERNS = [ { category: "private_key", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, }, { category: "bearer_value", pattern: /Authorization:\s*Bearer\s+[A-Za-z0-9._-]{8,}/i, }, { category: "credential_in_url", pattern: /postgres(?:ql)?:\/\/[^\s:/<>]+:[^\s@<>]+@/i, }, { category: "assigned_secret", pattern: /\b[A-Z][A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|API_KEY)[A-Z0-9_]*\b\s*[:=]\s*["']?(?!\s*(?:<|\$\{|\*|""|''|REDACTED|example))[A-Za-z0-9._/+=:-]{8,}/, }, { category: "personal_workspace_path", pattern: /\/(?:home|Users)\/(?!<)[^/\s`'"()]+\/[A-Za-z0-9._/ -]+/, }, { category: "email_account", pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/, }, { category: "phone_account", pattern: /(? !Number.isInteger(item) || item < 0 || item > 255)) { return false; } if (parts[0] === 10 || parts[0] === 127 || parts[0] === 0) return false; if (parts[0] === 169 && parts[1] === 254) return false; if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false; if (parts[0] === 192 && parts[1] === 168) return false; if (parts[0] >= 224) return false; return true; } export function scanSensitiveText(text, fileRef) { const findings = []; String(text).split(/\r?\n/).forEach((line, index) => { for (const { category, pattern } of TEXT_PATTERNS) { if (category === "credential_in_url" && (/\$\{/.test(line) || /<[^>]+>/.test(line))) { continue; } if (pattern.test(line)) { findings.push({ fileRef, line: index + 1, category }); } } const ipv4Values = line.match(/(? cell.trim()); if (!cells.some((cell) => cell === "`CURRENT`" || cell === "CURRENT")) { continue; } const contractCell = cells[1] ?? ""; for (const match of contractCell.matchAll(/`(docs\/[^`|]+\.(?:md|csv))`/g)) { const normalized = path.posix.normalize(match[1]); if (normalized.startsWith("docs/") && !normalized.includes("..")) { files.add(normalized); } } } return [...files].sort(); } export function resolveDefaultScanFiles(rootDir) { const registryPath = path.join(rootDir, CURRENT_DOCUMENT_REGISTRY); const currentDocuments = existsSync(registryPath) ? extractCurrentDocumentFiles(readFileSync(registryPath, "utf8")) : []; return [...new Set([...DEFAULT_SCAN_FILES, ...currentDocuments])]; } export function scanSensitiveContent({ rootDir, files }) { const findings = []; let filesScanned = 0; const scanFiles = files ?? resolveDefaultScanFiles(rootDir); for (const fileRef of scanFiles) { const file = path.join(rootDir, fileRef); if (!existsSync(file)) { continue; } filesScanned += 1; findings.push(...scanSensitiveText(readFileSync(file, "utf8"), fileRef)); } return { status: findings.length ? "FAIL" : "PASS", findingCount: findings.length, filesScanned, findings, }; } function main() { const rootDir = path.resolve(new URL("../..", import.meta.url).pathname); const result = scanSensitiveContent({ rootDir }); console.log(JSON.stringify(result, null, 2)); if (result.findingCount > 0) { process.exitCode = 1; } } const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMain) { main(); }