文件
hyb-oa/scripts/qa/check-sensitive-content.mjs
T

156 行
4.7 KiB
JavaScript

#!/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: /(?<!\d)1[3-9]\d{9}(?!\d)/,
},
];
function isPublicIpv4(value) {
const parts = value.split(".").map(Number);
if (parts.length !== 4 || parts.some((item) => !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(/(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)/g) || [];
if (ipv4Values.some(isPublicIpv4)) {
findings.push({ fileRef, line: index + 1, category: "public_ipv4" });
}
});
return findings;
}
export function extractCurrentDocumentFiles(matrixText) {
const files = new Set();
for (const line of String(matrixText).split(/\r?\n/)) {
if (!line.startsWith("|")) {
continue;
}
const cells = line
.split("|")
.slice(1, -1)
.map((cell) => 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();
}