diff --git a/.github/workflows/skyeye-credential-audit.yml b/.github/workflows/skyeye-credential-audit.yml new file mode 100644 index 00000000..cf762806 --- /dev/null +++ b/.github/workflows/skyeye-credential-audit.yml @@ -0,0 +1,49 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# 🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit) +# +# 守护: PER-ZY001 铸渊 (Zhuyuan) +# 系统: SYS-GLW-0001 +# 主控: TCS-0002∞ (冰朔) +# +# 功能: +# ① GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性校验 +# ② 依赖图扫描(工作流 + 脚本) +# ③ YAML 语法全量扫描 +# ④ 审计报告归档至 System_Logs/ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +name: "🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)" + +on: + schedule: + # 每天 UTC 14:00 (CST 22:00) 执行一次审计 + - cron: "0 14 * * *" + workflow_dispatch: + +permissions: + contents: write + +jobs: + credential-audit: + runs-on: ubuntu-latest + steps: + - name: "📦 Checkout" + uses: actions/checkout@v4 + + - name: "🛰️ Run Sky-Eye Credential Audit" + env: + GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} + run: node scripts/skyeye/credential-audit.js + + - name: "📝 Commit audit report" + if: always() + run: | + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan-bot@guanghulab.com" + git add System_Logs/ skyeye/logs/ + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "🛰️ [Sky-Eye Audit] credential audit report [skip ci]" + git push + fi diff --git a/.github/workflows/sync-repo-to-drive.yml b/.github/workflows/sync-repo-to-drive.yml index 6ea6e2ea..973445f7 100644 --- a/.github/workflows/sync-repo-to-drive.yml +++ b/.github/workflows/sync-repo-to-drive.yml @@ -49,6 +49,14 @@ jobs: printf '%s\n' "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" > "${SA_FILE}" chmod 600 "${SA_FILE}" + # 天眼密钥流校验:验证 JSON 完整性 + if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "${SA_FILE}" 2>/dev/null; then + echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT contains invalid JSON (possible truncation)" + rm -f "${SA_FILE}" + exit 1 + fi + echo "✅ Service account JSON validated" + mkdir -p "${HOME}/.config/rclone" { echo "[gdrive]" diff --git a/System_Logs/.gitkeep b/System_Logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/grid-db/deploy-drive-bridge.js b/scripts/grid-db/deploy-drive-bridge.js index 208b03bb..1a0e0ff9 100644 --- a/scripts/grid-db/deploy-drive-bridge.js +++ b/scripts/grid-db/deploy-drive-bridge.js @@ -303,8 +303,28 @@ async function deploy(commandFilePath) { return; } - // 认证 Google Drive API - const credentials = JSON.parse(serviceAccountJson); + // 认证 Google Drive API(天眼密钥流校验) + let credentials; + try { + const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator'); + const validation = validateServiceAccountJSON(serviceAccountJson); + if (!validation.valid) { + console.error('[deploy] 🔴 Credential validation failed:'); + console.error(formatDiagnosticReport(validation)); + writeReceipt(deploy_id, dev_id, 'failed', `Credential validation failed: ${validation.issues.join('; ')}`); + return; + } + credentials = validation.credentials; + console.log('[deploy] ✅ Service account credentials validated'); + } catch (err) { + // Fallback: if validator module is unavailable, try direct parse + try { + credentials = JSON.parse(serviceAccountJson); + } catch (parseErr) { + writeReceipt(deploy_id, dev_id, 'failed', `JSON parse error: ${parseErr.message}`); + return; + } + } const auth = new google.auth.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/drive'] diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js index ff4bddf5..1b6c0188 100644 --- a/scripts/grid-db/sync-to-drive.js +++ b/scripts/grid-db/sync-to-drive.js @@ -181,13 +181,26 @@ async function main() { return; } - // 解析 Service Account 凭证 + // 解析 Service Account 凭证(天眼密钥流校验) let credentials; try { - credentials = JSON.parse(serviceAccountJson); + const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator'); + const validation = validateServiceAccountJSON(serviceAccountJson); + if (!validation.valid) { + console.error('[sync-to-drive] 🔴 Credential validation failed:'); + console.error(formatDiagnosticReport(validation)); + process.exit(1); + } + credentials = validation.credentials; + console.log('[sync-to-drive] ✅ Service account credentials validated'); } catch (err) { - console.error('[sync-to-drive] Failed to parse service account JSON:', err.message); - process.exit(1); + // Fallback: if validator module is unavailable, try direct parse + try { + credentials = JSON.parse(serviceAccountJson); + } catch (parseErr) { + console.error('[sync-to-drive] Failed to parse service account JSON:', parseErr.message); + process.exit(1); + } } // 认证 Google Drive API diff --git a/scripts/skyeye/credential-audit.js b/scripts/skyeye/credential-audit.js new file mode 100644 index 00000000..fd59699b --- /dev/null +++ b/scripts/skyeye/credential-audit.js @@ -0,0 +1,268 @@ +#!/usr/bin/env node +/** + * scripts/skyeye/credential-audit.js + * + * 天眼全域系统审计 (Sky-Eye Credential Audit) + * + * 功能: + * ① 校验 GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性 + * ② 扫描所有依赖该密钥的工作流和脚本 + * ③ 验证 YAML 语法(.github/workflows/ 下所有配置文件) + * ④ 生成审计报告写入 System_Logs/ + * + * 环境变量: + * - GOOGLE_DRIVE_SERVICE_ACCOUNT: (可选) 用于实际校验 + * + * 用法:node scripts/skyeye/credential-audit.js + * + * 守护: PER-ZY001 铸渊 + * 系统: SYS-GLW-0001 + * 主控: TCS-0002∞ (冰朔) + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { validateServiceAccountJSON, formatDiagnosticReport } = require('./credential-validator'); + +const ROOT = path.resolve(__dirname, '../..'); +const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows'); +const SYSTEM_LOGS_DIR = path.join(ROOT, 'System_Logs'); + +function getTimestamp() { + return new Date().toISOString(); +} + +function getDateStr() { + return new Date().toISOString().slice(0, 10).replace(/-/g, ''); +} + +// ═══════════════════════════════════════════════ +// ① 密钥流校验 +// ═══════════════════════════════════════════════ + +function auditCredential() { + const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; + + if (!serviceAccountJson) { + return { + status: 'skipped', + reason: 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available in environment', + recommendation: 'Run this audit in a GitHub Actions workflow with the secret configured' + }; + } + + const validation = validateServiceAccountJSON(serviceAccountJson); + return { + status: validation.valid ? 'pass' : 'fail', + diagnostics: { + ...validation.diagnostics, + // Never expose credential values in reports + credentials: undefined + }, + issues: validation.issues, + report: formatDiagnosticReport(validation) + }; +} + +// ═══════════════════════════════════════════════ +// ② 依赖图扫描 +// ═══════════════════════════════════════════════ + +function scanCredentialDependencies() { + const results = { + workflows: [], + scripts: [], + total_dependents: 0 + }; + + const SEARCH_PATTERN = 'GOOGLE_DRIVE_SERVICE_ACCOUNT'; + + // 扫描 workflows + if (fs.existsSync(WORKFLOWS_DIR)) { + const files = fs.readdirSync(WORKFLOWS_DIR).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')); + for (const file of files) { + try { + const content = fs.readFileSync(path.join(WORKFLOWS_DIR, file), 'utf8'); + if (content.includes(SEARCH_PATTERN)) { + results.workflows.push(file); + } + } catch (e) { + // Skip unreadable files + } + } + } + + // 扫描 scripts + const scriptDirs = [ + path.join(ROOT, 'scripts'), + path.join(ROOT, 'scripts/grid-db'), + path.join(ROOT, 'scripts/skyeye') + ]; + + for (const dir of scriptDirs) { + if (!fs.existsSync(dir)) continue; + const files = fs.readdirSync(dir).filter(f => f.endsWith('.js')); + for (const file of files) { + try { + const content = fs.readFileSync(path.join(dir, file), 'utf8'); + if (content.includes(SEARCH_PATTERN)) { + const relPath = path.relative(ROOT, path.join(dir, file)); + results.scripts.push(relPath); + } + } catch (e) { + // Skip unreadable files + } + } + } + + results.total_dependents = results.workflows.length + results.scripts.length; + return results; +} + +// ═══════════════════════════════════════════════ +// ③ YAML 语法扫描 +// ═══════════════════════════════════════════════ + +function scanYAMLSyntax() { + const results = { + total: 0, + valid: 0, + issues: [] + }; + + if (!fs.existsSync(WORKFLOWS_DIR)) { + results.issues.push({ file: '.github/workflows/', error: 'Directory not found' }); + return results; + } + + const files = fs.readdirSync(WORKFLOWS_DIR).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')); + results.total = files.length; + + for (const file of files) { + const filePath = path.join(WORKFLOWS_DIR, file); + try { + const content = fs.readFileSync(filePath, 'utf8'); + + // Basic YAML structural checks (without full YAML parser) + const lines = content.split('\n'); + let hasName = false; + let hasOn = false; + let hasJobs = false; + let tabIssues = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^name:/.test(line)) hasName = true; + if (/^on:/.test(line) || /^'on':/.test(line) || /^"on":/.test(line)) hasOn = true; + if (/^jobs:/.test(line)) hasJobs = true; + // Check for tabs (YAML should use spaces) + if (line.includes('\t')) tabIssues++; + } + + const fileIssues = []; + if (!hasName) fileIssues.push('Missing top-level "name:" field'); + if (!hasOn) fileIssues.push('Missing top-level "on:" trigger'); + if (!hasJobs) fileIssues.push('Missing top-level "jobs:" section'); + if (tabIssues > 0) fileIssues.push(`${tabIssues} line(s) contain tabs (use spaces)`); + + if (fileIssues.length > 0) { + results.issues.push({ file, errors: fileIssues }); + } else { + results.valid++; + } + } catch (e) { + results.issues.push({ file, errors: [`Read error: ${e.message}`] }); + } + } + + return results; +} + +// ═══════════════════════════════════════════════ +// ④ 生成审计报告 +// ═══════════════════════════════════════════════ + +function run() { + console.log('╔════════════════════════════════════════════════════════╗'); + console.log('║ 🛰️ 天眼全域系统审计 (Sky-Eye Audit) ║'); + console.log('║ TCS-0002∞ → PER-ZY001 ║'); + console.log('╚════════════════════════════════════════════════════════╝'); + console.log(''); + + const auditReport = { + audit_id: `AUDIT-${getDateStr()}`, + timestamp: getTimestamp(), + system: 'SYS-GLW-0001', + guardian: 'PER-ZY001', + controller: 'TCS-0002∞', + credential_audit: auditCredential(), + dependency_scan: scanCredentialDependencies(), + yaml_scan: scanYAMLSyntax(), + summary: {} + }; + + // Summary + const credStatus = auditReport.credential_audit.status; + const yamlIssues = auditReport.yaml_scan.issues.length; + const totalDeps = auditReport.dependency_scan.total_dependents; + + auditReport.summary = { + credential_status: credStatus, + yaml_issues: yamlIssues, + yaml_total: auditReport.yaml_scan.total, + yaml_valid: auditReport.yaml_scan.valid, + total_credential_dependents: totalDeps, + overall_status: credStatus === 'pass' && yamlIssues === 0 ? 'GREEN' : 'RED' + }; + + // Console output + console.log(`[Sky-Eye Audit] Credential status: ${credStatus}`); + if (auditReport.credential_audit.report) { + console.log(''); + console.log(auditReport.credential_audit.report); + } + console.log(''); + console.log(`[Sky-Eye Audit] Credential dependents: ${totalDeps} (${auditReport.dependency_scan.workflows.length} workflows, ${auditReport.dependency_scan.scripts.length} scripts)`); + console.log(`[Sky-Eye Audit] YAML scan: ${auditReport.yaml_scan.valid}/${auditReport.yaml_scan.total} valid`); + + if (yamlIssues > 0) { + console.log(`[Sky-Eye Audit] YAML issues found in ${yamlIssues} file(s):`); + for (const issue of auditReport.yaml_scan.issues) { + console.log(` - ${issue.file}: ${issue.errors ? issue.errors.join('; ') : issue.error}`); + } + } + + console.log(''); + console.log(`[Sky-Eye Audit] Overall: ${auditReport.summary.overall_status === 'GREEN' ? '✅ 绿色信号' : '🔴 逻辑红区'}`); + + // Write report to System_Logs/ + if (!fs.existsSync(SYSTEM_LOGS_DIR)) { + fs.mkdirSync(SYSTEM_LOGS_DIR, { recursive: true }); + } + const reportPath = path.join(SYSTEM_LOGS_DIR, `credential-audit-${getDateStr()}.json`); + // Ensure no credential data leaks into the report + const safeReport = JSON.parse(JSON.stringify(auditReport)); + if (safeReport.credential_audit && safeReport.credential_audit.diagnostics) { + delete safeReport.credential_audit.diagnostics.context_at_error; + } + fs.writeFileSync(reportPath, JSON.stringify(safeReport, null, 2) + '\n', 'utf8'); + console.log(`[Sky-Eye Audit] Report saved: ${reportPath}`); + + // Also write to skyeye logs + const skyeyeLogDir = path.join(ROOT, 'skyeye/logs/daily'); + if (!fs.existsSync(skyeyeLogDir)) { + fs.mkdirSync(skyeyeLogDir, { recursive: true }); + } + const skyeyeLogPath = path.join(skyeyeLogDir, `credential-audit-${getDateStr()}.json`); + fs.writeFileSync(skyeyeLogPath, JSON.stringify(safeReport, null, 2) + '\n', 'utf8'); + + console.log(''); + console.log('---AUDIT_RESULT_JSON---'); + console.log(JSON.stringify(safeReport, null, 2)); + + return auditReport; +} + +run(); diff --git a/scripts/skyeye/credential-validator.js b/scripts/skyeye/credential-validator.js new file mode 100644 index 00000000..de72b3a2 --- /dev/null +++ b/scripts/skyeye/credential-validator.js @@ -0,0 +1,233 @@ +#!/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); +} diff --git a/scripts/tcs-semantic-landing.js b/scripts/tcs-semantic-landing.js index e5e7ff64..2a556a45 100644 --- a/scripts/tcs-semantic-landing.js +++ b/scripts/tcs-semantic-landing.js @@ -40,7 +40,22 @@ const LANDING_SUBFOLDER = 'tcs-semantic-landing'; // ═══════════════════════════════════════════════ function buildAuth(serviceAccountJson) { - const credentials = JSON.parse(serviceAccountJson); + let credentials; + try { + const { validateServiceAccountJSON, formatDiagnosticReport } = require('./skyeye/credential-validator'); + const validation = validateServiceAccountJSON(serviceAccountJson); + if (!validation.valid) { + console.error('[semantic-landing] 🔴 Credential validation failed:'); + console.error(formatDiagnosticReport(validation)); + throw new Error('Service account credential validation failed'); + } + credentials = validation.credentials; + console.log('[semantic-landing] ✅ Service account credentials validated'); + } catch (err) { + if (err.message === 'Service account credential validation failed') throw err; + // Fallback: if validator module is unavailable, try direct parse + credentials = JSON.parse(serviceAccountJson); + } return new google.auth.GoogleAuth({ credentials, scopes: [ diff --git a/skyeye/scripts/self-healer.js b/skyeye/scripts/self-healer.js index b24e6e6c..14181ec9 100644 --- a/skyeye/scripts/self-healer.js +++ b/skyeye/scripts/self-healer.js @@ -182,7 +182,8 @@ function ensureDirectories() { 'skyeye/logs/weekly', 'buffer/inbox', 'buffer/staging', - 'buffer/processed' + 'buffer/processed', + 'System_Logs' ]; for (const dir of requiredDirs) { @@ -197,6 +198,37 @@ function ensureDirectories() { return results; } +/** + * 天眼密钥流校验 — 验证 GOOGLE_DRIVE_SERVICE_ACCOUNT 环境变量 + * 仅在环境变量可用时执行(CI 环境) + */ +function validateCredentials() { + const result = { status: 'skipped', issues: [] }; + + const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; + if (!serviceAccountJson) { + result.reason = 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available'; + return result; + } + + try { + const { validateServiceAccountJSON } = require('../../scripts/skyeye/credential-validator'); + const validation = validateServiceAccountJSON(serviceAccountJson); + result.status = validation.valid ? 'pass' : 'fail'; + result.issues = validation.issues; + if (!validation.valid) { + console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`); + } else { + console.log(' [CREDENTIAL] ✅ Service account credentials valid'); + } + } catch (e) { + result.status = 'error'; + result.reason = `Validator load error: ${e.message}`; + } + + return result; +} + function run() { console.log(`[SkyEye Self-Healer] Starting self-heal process`); console.log(`[SkyEye Self-Healer] Timestamp: ${getTimestamp()}`); @@ -209,6 +241,7 @@ function run() { log_cleanup: cleanExpiredLogs(), guard_restarts: restartSuspendedGuards(), directory_fixes: ensureDirectories(), + credential_check: validateCredentials(), summary: {} }; @@ -216,7 +249,8 @@ function run() { configs_repaired: healResult.guard_repairs.repaired.length, files_cleaned: healResult.buffer_cleanup.files_cleaned + healResult.log_cleanup.files_cleaned, guards_restarted: healResult.guard_restarts.restarted.length, - directories_created: healResult.directory_fixes.created.length + directories_created: healResult.directory_fixes.created.length, + credential_status: healResult.credential_check.status }; // Write log @@ -229,6 +263,7 @@ function run() { console.log(`[SkyEye Self-Healer] Files cleaned: ${healResult.summary.files_cleaned}`); console.log(`[SkyEye Self-Healer] Guards restarted: ${healResult.summary.guards_restarted}`); console.log(`[SkyEye Self-Healer] Dirs created: ${healResult.summary.directories_created}`); + console.log(`[SkyEye Self-Healer] Credential status: ${healResult.summary.credential_status}`); console.log(`[SkyEye Self-Healer] Log saved: ${logPath}`); console.log('---HEAL_RESULT_JSON---');