guanghulab/scripts/skyeye/credential-audit.js

267 lines
9.6 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* scripts/skyeye/credential-audit.js
*
* 天眼全域系统审计 (Sky-Eye Credential Audit)
*
* 功能
* 校验 OAuth2 凭据环境变量完整性
* 扫描所有依赖 Drive 凭据的工作流和脚本
* 验证 YAML 语法.github/workflows/ 下所有配置文件
* 生成审计报告写入 System_Logs/
*
* 环境变量
* - GDRIVE_CLIENT_ID: (可选) OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: (可选) OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: (可选) 长效刷新令牌
*
* 用法node scripts/skyeye/credential-audit.js
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
* 主控: TCS-0002 (冰朔)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { validateOAuth2Credentials, 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 hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
if (!hasAnyVar) {
return {
status: 'skipped',
reason: 'OAuth2 credentials not available in environment (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)',
recommendation: 'Run this audit in a GitHub Actions workflow with the secrets configured'
};
}
const validation = validateOAuth2Credentials();
return {
status: validation.valid ? 'pass' : 'fail',
diagnostics: validation.diagnostics,
issues: validation.issues,
report: formatDiagnosticReport(validation)
};
}
// ═══════════════════════════════════════════════
// ② 依赖图扫描
// ═══════════════════════════════════════════════
function scanCredentialDependencies() {
const results = {
workflows: [],
scripts: [],
total_dependents: 0
};
const SEARCH_PATTERN = 'GDRIVE_CLIENT_ID';
// 扫描 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();