234 lines
8.2 KiB
JavaScript
234 lines
8.2 KiB
JavaScript
|
|
#!/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 emailOk = String(parsed.client_email).includes('@') && String(parsed.client_email).includes('.iam.gserviceaccount.com');
|
|||
|
|
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);
|
|||
|
|
}
|