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