🛰️ [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}"
|
||||
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]"
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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: [
|
||||
|
|
|
|||
|
|
@ -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---');
|
||||
|
|
|
|||
Loading…
Reference in New Issue