zhizhi/scripts/skyeye/credential-validator.js

235 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* scripts/skyeye/credential-validator.js
*
* 天眼密钥流完整性校验器 (Sky-Eye Credential Validator)
*
* 功能:对 Google Drive Service Account JSON 执行结构化检测
* - JSON 语法完整性(检测未闭合 {} / 截断)
* - 必需字段存在性验证
* - 字段值格式校验private_key PEM 格式等)
*
* 可作为模块 require() 使用,也可直接运行:
* GOOGLE_DRIVE_SERVICE_ACCOUNT='...' node credential-validator.js
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
* 主控: TCS-0002∞ (冰朔)
*/
'use strict';
// Google Service Account JSON 必需字段
const REQUIRED_FIELDS = [
'type',
'project_id',
'private_key_id',
'private_key',
'client_email',
'client_id',
'auth_uri',
'token_uri'
];
/**
* 验证 Service Account JSON 字符串
* @param {string} jsonStr - JSON 字符串
* @returns {{ valid: boolean, credentials: object|null, issues: string[], diagnostics: object }}
*/
function validateServiceAccountJSON(jsonStr) {
const result = {
valid: false,
credentials: null,
issues: [],
diagnostics: {
input_length: 0,
is_empty: true,
json_parseable: false,
is_object: false,
has_all_required_fields: false,
missing_fields: [],
field_checks: {},
truncation_suspected: false
}
};
// ① 空值检查
if (!jsonStr || typeof jsonStr !== 'string') {
result.issues.push('Credential string is empty or not a string');
return result;
}
const trimmed = jsonStr.trim();
result.diagnostics.input_length = trimmed.length;
result.diagnostics.is_empty = trimmed.length === 0;
if (trimmed.length === 0) {
result.issues.push('Credential string is empty after trimming');
return result;
}
// ② 基本结构检查(闭合括号)
if (!trimmed.startsWith('{')) {
result.issues.push(`JSON does not start with '{' (starts with '${trimmed[0]}')`);
}
if (!trimmed.endsWith('}')) {
result.issues.push(`JSON does not end with '}' (ends with '${trimmed[trimmed.length - 1]}')`);
result.diagnostics.truncation_suspected = true;
}
// 检查大括号平衡
let braceDepth = 0;
for (let i = 0; i < trimmed.length; i++) {
if (trimmed[i] === '{') braceDepth++;
else if (trimmed[i] === '}') braceDepth--;
}
if (braceDepth !== 0) {
result.issues.push(`Unbalanced braces: depth=${braceDepth} (${braceDepth > 0 ? 'missing closing }' : 'extra closing }'})`);
result.diagnostics.truncation_suspected = braceDepth > 0;
}
// ③ JSON 解析
let parsed;
try {
parsed = JSON.parse(trimmed);
result.diagnostics.json_parseable = true;
} catch (err) {
result.issues.push(`JSON parse error: ${err.message}`);
// 尝试诊断截断位置
const posMatch = err.message.match(/position (\d+)/);
if (posMatch) {
const pos = parseInt(posMatch[1], 10);
result.diagnostics.error_position = pos;
result.diagnostics.context_at_error = trimmed.substring(
Math.max(0, pos - 30),
Math.min(trimmed.length, pos + 30)
);
}
return result;
}
// ④ 类型检查
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
result.issues.push('Parsed JSON is not a plain object');
return result;
}
result.diagnostics.is_object = true;
result.credentials = parsed;
// ⑤ 必需字段检查
for (const field of REQUIRED_FIELDS) {
if (!(field in parsed) || parsed[field] === undefined || parsed[field] === null || parsed[field] === '') {
result.diagnostics.missing_fields.push(field);
result.issues.push(`Missing or empty required field: '${field}'`);
}
}
result.diagnostics.has_all_required_fields = result.diagnostics.missing_fields.length === 0;
// ⑥ 字段格式校验
if (parsed.type) {
const typeOk = parsed.type === 'service_account';
result.diagnostics.field_checks.type = typeOk ? 'ok' : `expected 'service_account', got '${parsed.type}'`;
if (!typeOk) result.issues.push(`Field 'type' should be 'service_account', got '${parsed.type}'`);
}
if (parsed.private_key) {
const pkStr = String(parsed.private_key);
const hasBegin = pkStr.includes('-----BEGIN');
const hasEnd = pkStr.includes('-----END');
result.diagnostics.field_checks.private_key_format = hasBegin && hasEnd ? 'ok' : 'PEM markers missing';
if (!hasBegin || !hasEnd) {
result.issues.push('Field \'private_key\' does not contain valid PEM BEGIN/END markers');
result.diagnostics.truncation_suspected = true;
}
}
if (parsed.client_email) {
const emailStr = String(parsed.client_email);
const emailOk = /^[^@]+@[^@]+\.iam\.gserviceaccount\.com$/.test(emailStr);
result.diagnostics.field_checks.client_email_format = emailOk ? 'ok' : 'not a valid service account email';
if (!emailOk) result.issues.push('Field \'client_email\' does not match service account email pattern');
}
// ⑦ 最终判定
result.valid = result.issues.length === 0;
return result;
}
/**
* 生成人类可读的诊断报告
* @param {object} validationResult - validateServiceAccountJSON 的返回值
* @returns {string}
*/
function formatDiagnosticReport(validationResult) {
const r = validationResult;
const lines = [];
lines.push('╔════════════════════════════════════════════════════════╗');
lines.push('║ 🛰️ 天眼密钥流完整性校验报告 (Credential Audit) ║');
lines.push('╚════════════════════════════════════════════════════════╝');
lines.push('');
lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`);
lines.push(`输入长度: ${r.diagnostics.input_length} bytes`);
lines.push(`JSON 可解析: ${r.diagnostics.json_parseable ? '是' : '否'}`);
lines.push(`是否为对象: ${r.diagnostics.is_object ? '是' : '否'}`);
lines.push(`全部必需字段: ${r.diagnostics.has_all_required_fields ? '齐全' : '缺失'}`);
lines.push(`截断嫌疑: ${r.diagnostics.truncation_suspected ? '⚠️ 是' : '否'}`);
if (r.diagnostics.missing_fields.length > 0) {
lines.push(`缺失字段: ${r.diagnostics.missing_fields.join(', ')}`);
}
if (r.diagnostics.error_position !== undefined) {
lines.push(`错误位置: position ${r.diagnostics.error_position}`);
lines.push(`上下文: ...${r.diagnostics.context_at_error}...`);
}
if (Object.keys(r.diagnostics.field_checks).length > 0) {
lines.push('');
lines.push('字段检查:');
for (const [k, v] of Object.entries(r.diagnostics.field_checks)) {
lines.push(` ${k}: ${v}`);
}
}
if (r.issues.length > 0) {
lines.push('');
lines.push('发现问题:');
r.issues.forEach((issue, i) => {
lines.push(` ${i + 1}. ${issue}`);
});
}
return lines.join('\n');
}
// ═══════════════════════════════════════════════
// 模块导出
// ═══════════════════════════════════════════════
module.exports = { validateServiceAccountJSON, formatDiagnosticReport, REQUIRED_FIELDS };
// ═══════════════════════════════════════════════
// CLI 模式
// ═══════════════════════════════════════════════
if (require.main === module) {
const jsonStr = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
if (!jsonStr) {
console.error('[credential-validator] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
process.exit(1);
}
const result = validateServiceAccountJSON(jsonStr);
console.log(formatDiagnosticReport(result));
console.log('');
console.log('---CREDENTIAL_AUDIT_JSON---');
// 不输出 credentials 本身以避免泄露密钥
const safeResult = { ...result, credentials: result.credentials ? '[REDACTED]' : null };
console.log(JSON.stringify(safeResult, null, 2));
process.exit(result.valid ? 0 : 1);
}