🛰️ [Sky-Eye Audit] credential validation, hardened error handling, audit workflow
- Add credential-validator.js: reusable JSON structure validator for service account keys - Add credential-audit.js: full sky-eye audit (credential + dependency scan + YAML check) - Add skyeye-credential-audit.yml: daily audit workflow - Harden sync-to-drive.js: use validator with diagnostic reporting - Harden tcs-semantic-landing.js: add try-catch to buildAuth() - Harden deploy-drive-bridge.js: add try-catch with receipt writing - Enhance sync-repo-to-drive.yml: add JSON validation before rclone config - Enhance self-healer.js: add credential validation + System_Logs directory - Create System_Logs/ directory for audit reports Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/8bc25538-ab95-4520-a72b-ff0d8405c7c7
This commit is contained in:
parent
85fec1406d
commit
6d493042c5
|
|
@ -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
|
||||||
|
|
@ -49,6 +49,14 @@ jobs:
|
||||||
printf '%s\n' "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" > "${SA_FILE}"
|
printf '%s\n' "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" > "${SA_FILE}"
|
||||||
chmod 600 "${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"
|
mkdir -p "${HOME}/.config/rclone"
|
||||||
{
|
{
|
||||||
echo "[gdrive]"
|
echo "[gdrive]"
|
||||||
|
|
|
||||||
|
|
@ -303,8 +303,28 @@ async function deploy(commandFilePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 认证 Google Drive API
|
// 认证 Google Drive API(天眼密钥流校验)
|
||||||
const credentials = JSON.parse(serviceAccountJson);
|
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({
|
const auth = new google.auth.GoogleAuth({
|
||||||
credentials,
|
credentials,
|
||||||
scopes: ['https://www.googleapis.com/auth/drive']
|
scopes: ['https://www.googleapis.com/auth/drive']
|
||||||
|
|
|
||||||
|
|
@ -181,13 +181,26 @@ async function main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 Service Account 凭证
|
// 解析 Service Account 凭证(天眼密钥流校验)
|
||||||
let credentials;
|
let credentials;
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error('[sync-to-drive] Failed to parse service account JSON:', err.message);
|
// Fallback: if validator module is unavailable, try direct parse
|
||||||
process.exit(1);
|
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
|
// 认证 Google Drive API
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,22 @@ const LANDING_SUBFOLDER = 'tcs-semantic-landing';
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
function buildAuth(serviceAccountJson) {
|
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({
|
return new google.auth.GoogleAuth({
|
||||||
credentials,
|
credentials,
|
||||||
scopes: [
|
scopes: [
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,8 @@ function ensureDirectories() {
|
||||||
'skyeye/logs/weekly',
|
'skyeye/logs/weekly',
|
||||||
'buffer/inbox',
|
'buffer/inbox',
|
||||||
'buffer/staging',
|
'buffer/staging',
|
||||||
'buffer/processed'
|
'buffer/processed',
|
||||||
|
'System_Logs'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const dir of requiredDirs) {
|
for (const dir of requiredDirs) {
|
||||||
|
|
@ -197,6 +198,37 @@ function ensureDirectories() {
|
||||||
return results;
|
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() {
|
function run() {
|
||||||
console.log(`[SkyEye Self-Healer] Starting self-heal process`);
|
console.log(`[SkyEye Self-Healer] Starting self-heal process`);
|
||||||
console.log(`[SkyEye Self-Healer] Timestamp: ${getTimestamp()}`);
|
console.log(`[SkyEye Self-Healer] Timestamp: ${getTimestamp()}`);
|
||||||
|
|
@ -209,6 +241,7 @@ function run() {
|
||||||
log_cleanup: cleanExpiredLogs(),
|
log_cleanup: cleanExpiredLogs(),
|
||||||
guard_restarts: restartSuspendedGuards(),
|
guard_restarts: restartSuspendedGuards(),
|
||||||
directory_fixes: ensureDirectories(),
|
directory_fixes: ensureDirectories(),
|
||||||
|
credential_check: validateCredentials(),
|
||||||
summary: {}
|
summary: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -216,7 +249,8 @@ function run() {
|
||||||
configs_repaired: healResult.guard_repairs.repaired.length,
|
configs_repaired: healResult.guard_repairs.repaired.length,
|
||||||
files_cleaned: healResult.buffer_cleanup.files_cleaned + healResult.log_cleanup.files_cleaned,
|
files_cleaned: healResult.buffer_cleanup.files_cleaned + healResult.log_cleanup.files_cleaned,
|
||||||
guards_restarted: healResult.guard_restarts.restarted.length,
|
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
|
// Write log
|
||||||
|
|
@ -229,6 +263,7 @@ function run() {
|
||||||
console.log(`[SkyEye Self-Healer] Files cleaned: ${healResult.summary.files_cleaned}`);
|
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] Guards restarted: ${healResult.summary.guards_restarted}`);
|
||||||
console.log(`[SkyEye Self-Healer] Dirs created: ${healResult.summary.directories_created}`);
|
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(`[SkyEye Self-Healer] Log saved: ${logPath}`);
|
||||||
|
|
||||||
console.log('---HEAL_RESULT_JSON---');
|
console.log('---HEAL_RESULT_JSON---');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue