review-guard.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { execFileSync } from 'node:child_process';
  2. import { existsSync, readFileSync } from 'node:fs';
  3. import { fileURLToPath } from 'node:url';
  4. import { relative, resolve } from 'node:path';
  5. const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
  6. const failures = [];
  7. const warnings = [];
  8. const secretTargets = [
  9. 'server.js',
  10. 'server',
  11. 'src',
  12. 'package.json',
  13. 'proxy.conf.json',
  14. ];
  15. const cloudFallbackFiles = [
  16. 'cloud-functions/06-voiceManager.js',
  17. 'cloud-functions/07-quicklyVideo.js',
  18. 'cloud-functions/08-proxyHub.js',
  19. 'cloud-functions/09-uploadManager.js',
  20. 'cloud-functions/11-jimengManager.js',
  21. 'cloud-functions/12-douyinManager.js',
  22. 'cloud-functions/13-douyinInsightManager.js',
  23. ];
  24. const largeFileBudgets = [
  25. { file: 'src/app/app.ts', maxLines: 3000 },
  26. { file: 'src/app/app.css', maxLines: 7000 },
  27. { file: 'server.js', maxLines: 1800 },
  28. { file: 'src/app/pages/pipelines/topic-to-video/topic-to-video.component.ts', maxLines: 1200 },
  29. ];
  30. const secretPatterns = [
  31. { name: 'Bearer r token', pattern: /Bearer\s+r:[A-Za-z0-9_-]{20,}/g },
  32. { name: 'r token', pattern: /['"`]r:[A-Za-z0-9_-]{20,}['"`]/g },
  33. { name: 'sk key', pattern: /['"`]sk-[A-Za-z0-9_-]{20,}['"`]/g },
  34. ];
  35. const contextualSecretPatterns = [
  36. {
  37. name: 'long token/key literal',
  38. pattern: /\b(token|secret|apiKey|appSecret|accessKey|currentToken)\b[^;\n=:{]*[:=]\s*['"`][A-Za-z0-9+/=_-]{32,}['"`]/gi,
  39. },
  40. ];
  41. function runGit(args) {
  42. try {
  43. return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
  44. } catch {
  45. return '';
  46. }
  47. }
  48. function listTrackedFiles(paths) {
  49. const files = runGit(['ls-files', ...paths]);
  50. return files ? files.split(/\r?\n/).filter(Boolean) : [];
  51. }
  52. function read(file) {
  53. return readFileSync(resolve(root, file), 'utf8');
  54. }
  55. function lineOf(content, index) {
  56. return content.slice(0, index).split(/\r?\n/).length;
  57. }
  58. function formatFile(file) {
  59. return relative(root, resolve(root, file)).replace(/\\/g, '/');
  60. }
  61. const trackedEnv = runGit(['ls-files', '.env']);
  62. if (trackedEnv) {
  63. failures.push('根目录 .env 已被 Git 跟踪,请移出版本库,只保留 .env.example。');
  64. }
  65. for (const file of listTrackedFiles(secretTargets)) {
  66. if (!existsSync(resolve(root, file))) continue;
  67. if (file.endsWith('.map') || file.endsWith('.png') || file.endsWith('.jpg') || file.endsWith('.jpeg') || file.endsWith('.webp')) continue;
  68. const content = read(file);
  69. for (const item of secretPatterns) {
  70. for (const match of content.matchAll(item.pattern)) {
  71. failures.push(`${formatFile(file)}:${lineOf(content, match.index || 0)} 命中疑似真实凭证:${item.name}`);
  72. }
  73. }
  74. for (const item of contextualSecretPatterns) {
  75. for (const match of content.matchAll(item.pattern)) {
  76. failures.push(`${formatFile(file)}:${lineOf(content, match.index || 0)} 命中疑似真实凭证:${item.name}`);
  77. }
  78. }
  79. }
  80. for (const file of cloudFallbackFiles) {
  81. if (!existsSync(resolve(root, file))) continue;
  82. const content = read(file);
  83. for (const item of secretPatterns) {
  84. if (item.pattern.test(content)) {
  85. warnings.push(`${formatFile(file)} 保留云函数源码兜底凭证。当前按用户要求暂缓治理,后续确认云函数部署窗口后再处理。`);
  86. break;
  87. }
  88. }
  89. }
  90. for (const item of largeFileBudgets) {
  91. if (!existsSync(resolve(root, item.file))) continue;
  92. const count = read(item.file).split(/\r?\n/).length;
  93. if (count > item.maxLines) {
  94. warnings.push(`${formatFile(item.file)} 当前 ${count} 行,超过治理阈值 ${item.maxLines} 行。`);
  95. }
  96. }
  97. if (warnings.length) {
  98. console.warn('\nReview guard warnings:');
  99. for (const warning of warnings) console.warn(`- ${warning}`);
  100. }
  101. if (failures.length) {
  102. console.error('\nReview guard failed:');
  103. for (const failure of failures) console.error(`- ${failure}`);
  104. process.exit(1);
  105. }
  106. console.log('Review guard passed');