code-abyss 1.6.15 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/bin/install.js +25 -4
  2. package/package.json +2 -2
  3. package/skills/SKILL.md +24 -16
  4. package/skills/domains/ai/SKILL.md +2 -2
  5. package/skills/domains/ai/prompt-and-eval.md +279 -0
  6. package/skills/domains/architecture/SKILL.md +2 -3
  7. package/skills/domains/architecture/security-arch.md +87 -0
  8. package/skills/domains/data-engineering/SKILL.md +188 -26
  9. package/skills/domains/development/SKILL.md +1 -4
  10. package/skills/domains/devops/SKILL.md +3 -5
  11. package/skills/domains/devops/performance.md +63 -0
  12. package/skills/domains/devops/testing.md +97 -0
  13. package/skills/domains/frontend-design/SKILL.md +12 -3
  14. package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
  15. package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
  16. package/skills/domains/frontend-design/engineering.md +287 -0
  17. package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
  18. package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
  19. package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
  20. package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
  21. package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
  22. package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
  23. package/skills/domains/infrastructure/SKILL.md +174 -34
  24. package/skills/domains/mobile/SKILL.md +211 -21
  25. package/skills/domains/orchestration/SKILL.md +1 -0
  26. package/skills/domains/security/SKILL.md +4 -6
  27. package/skills/domains/security/blue-team.md +57 -0
  28. package/skills/domains/security/red-team.md +54 -0
  29. package/skills/domains/security/threat-intel.md +50 -0
  30. package/skills/orchestration/multi-agent/SKILL.md +195 -46
  31. package/skills/run_skill.js +134 -0
  32. package/skills/tools/gen-docs/SKILL.md +6 -4
  33. package/skills/tools/gen-docs/scripts/doc_generator.js +349 -0
  34. package/skills/tools/verify-change/SKILL.md +8 -6
  35. package/skills/tools/verify-change/scripts/change_analyzer.js +270 -0
  36. package/skills/tools/verify-module/SKILL.md +6 -4
  37. package/skills/tools/verify-module/scripts/module_scanner.js +145 -0
  38. package/skills/tools/verify-quality/SKILL.md +5 -3
  39. package/skills/tools/verify-quality/scripts/quality_checker.js +276 -0
  40. package/skills/tools/verify-security/SKILL.md +7 -5
  41. package/skills/tools/verify-security/scripts/security_scanner.js +133 -0
  42. package/skills/domains/COVERAGE_PLAN.md +0 -232
  43. package/skills/domains/ai/model-evaluation.md +0 -790
  44. package/skills/domains/ai/prompt-engineering.md +0 -703
  45. package/skills/domains/architecture/compliance.md +0 -299
  46. package/skills/domains/architecture/data-security.md +0 -184
  47. package/skills/domains/data-engineering/data-pipeline.md +0 -762
  48. package/skills/domains/data-engineering/data-quality.md +0 -894
  49. package/skills/domains/data-engineering/stream-processing.md +0 -791
  50. package/skills/domains/development/dart.md +0 -963
  51. package/skills/domains/development/kotlin.md +0 -834
  52. package/skills/domains/development/php.md +0 -659
  53. package/skills/domains/development/swift.md +0 -755
  54. package/skills/domains/devops/e2e-testing.md +0 -914
  55. package/skills/domains/devops/performance-testing.md +0 -734
  56. package/skills/domains/devops/testing-strategy.md +0 -667
  57. package/skills/domains/frontend-design/build-tools.md +0 -743
  58. package/skills/domains/frontend-design/performance.md +0 -734
  59. package/skills/domains/frontend-design/testing.md +0 -699
  60. package/skills/domains/infrastructure/gitops.md +0 -735
  61. package/skills/domains/infrastructure/iac.md +0 -855
  62. package/skills/domains/infrastructure/kubernetes.md +0 -1018
  63. package/skills/domains/mobile/android-dev.md +0 -979
  64. package/skills/domains/mobile/cross-platform.md +0 -795
  65. package/skills/domains/mobile/ios-dev.md +0 -931
  66. package/skills/domains/security/secrets-management.md +0 -834
  67. package/skills/domains/security/supply-chain.md +0 -931
  68. package/skills/domains/security/threat-modeling.md +0 -828
  69. package/skills/run_skill.py +0 -88
  70. package/skills/tests/README.md +0 -225
  71. package/skills/tests/SUMMARY.md +0 -362
  72. package/skills/tests/__init__.py +0 -3
  73. package/skills/tests/test_change_analyzer.py +0 -558
  74. package/skills/tests/test_doc_generator.py +0 -538
  75. package/skills/tests/test_module_scanner.py +0 -376
  76. package/skills/tests/test_quality_checker.py +0 -516
  77. package/skills/tests/test_security_scanner.py +0 -426
  78. package/skills/tools/gen-docs/scripts/doc_generator.py +0 -491
  79. package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
  80. package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
  81. package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
  82. package/skills/tools/verify-security/scripts/security_scanner.py +0 -368
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const REQUIRED_FILES = { 'README.md': '模块说明文档', 'DESIGN.md': '设计决策文档' };
8
+ const ALT_SRC_DIRS = ['src', 'lib', 'pkg', 'internal', 'cmd', 'app'];
9
+ const ALT_TEST_DIRS = ['tests', 'test', '__tests__', 'spec'];
10
+ const ROOT_SCRIPT_FILES = new Set(['install.sh', 'uninstall.sh', 'install.ps1', 'uninstall.ps1', 'Dockerfile', 'Makefile']);
11
+ const CODE_EXTS = new Set(['.py', '.go', '.rs', '.ts', '.js', '.java', '.sh', '.ps1']);
12
+ const TEST_PATTERNS = ['test_', '_test.', '.test.', 'spec_', '_spec.'];
13
+
14
+ function scanStructure(p, depth = 3) {
15
+ const s = { name: path.basename(p), type: 'dir', children: [] };
16
+ if (depth <= 0) return s;
17
+ try {
18
+ for (const name of fs.readdirSync(p).sort()) {
19
+ if (name.startsWith('.')) continue;
20
+ const full = path.join(p, name);
21
+ const stat = fs.statSync(full);
22
+ if (stat.isFile()) s.children.push({ name, type: 'file', size: stat.size });
23
+ else if (stat.isDirectory()) s.children.push(scanStructure(full, depth - 1));
24
+ }
25
+ } catch {}
26
+ return s;
27
+ }
28
+
29
+ function rglob(dir, test) {
30
+ try {
31
+ for (const name of fs.readdirSync(dir)) {
32
+ const full = path.join(dir, name);
33
+ try {
34
+ const stat = fs.statSync(full);
35
+ if (stat.isFile() && test(name)) return true;
36
+ if (stat.isDirectory()) { if (rglob(full, test)) return true; }
37
+ } catch {}
38
+ }
39
+ } catch {}
40
+ return false;
41
+ }
42
+
43
+ function scanModule(target) {
44
+ const modulePath = path.resolve(target);
45
+ const issues = [];
46
+ const add = (severity, message, p) => issues.push({ severity, message, path: p || null });
47
+
48
+ if (!fs.existsSync(modulePath)) { add('error', `路径不存在: ${modulePath}`); return { modulePath, issues, structure: {} }; }
49
+ if (!fs.statSync(modulePath).isDirectory()) { add('error', `不是目录: ${modulePath}`); return { modulePath, issues, structure: {} }; }
50
+
51
+ const structure = scanStructure(modulePath);
52
+
53
+ // required files
54
+ for (const [file, desc] of Object.entries(REQUIRED_FILES)) {
55
+ const fp = path.join(modulePath, file);
56
+ if (!fs.existsSync(fp)) add('error', `缺少必需文档: ${file} (${desc})`, fp);
57
+ else if (fs.statSync(fp).size < 50) add('warning', `文档内容过少: ${file} (< 50 bytes)`, fp);
58
+ }
59
+
60
+ // source dirs
61
+ let srcFound = ALT_SRC_DIRS.some(d => { try { return fs.statSync(path.join(modulePath, d)).isDirectory(); } catch { return false; } });
62
+ const entries = fs.readdirSync(modulePath);
63
+ const rootCode = entries.filter(n => { try { const s = fs.statSync(path.join(modulePath, n)); return s.isFile() && CODE_EXTS.has(path.extname(n)); } catch { return false; } });
64
+ const rootScript = entries.filter(n => { try { return fs.statSync(path.join(modulePath, n)).isFile() && ROOT_SCRIPT_FILES.has(n); } catch { return false; } });
65
+ if (rootCode.length || rootScript.length) { srcFound = true; if (rootCode.length > 5) add('warning', `根目录代码文件过多 (${rootCode.length}个),建议整理到 src/ 目录`); }
66
+ if (!srcFound) add('warning', '未找到源码目录或代码文件');
67
+
68
+ // test dirs
69
+ let testFound = ALT_TEST_DIRS.some(d => { try { return fs.statSync(path.join(modulePath, d)).isDirectory(); } catch { return false; } });
70
+ if (!testFound) testFound = rglob(modulePath, n => TEST_PATTERNS.some(p => n.includes(p)));
71
+ if (!testFound) add('warning', '未找到测试目录或测试文件');
72
+
73
+ // doc quality
74
+ const readme = path.join(modulePath, 'README.md');
75
+ if (fs.existsSync(readme)) {
76
+ const c = fs.readFileSync(readme, 'utf-8');
77
+ if (!c.includes('#')) add('warning', 'README.md 缺少标题', readme);
78
+ if (!['usage', 'install', '使用', '安装', 'example', '示例'].some(k => c.toLowerCase().includes(k))) add('info', 'README.md 建议添加使用说明或示例', readme);
79
+ }
80
+ const design = path.join(modulePath, 'DESIGN.md');
81
+ if (fs.existsSync(design)) {
82
+ const c = fs.readFileSync(design, 'utf-8');
83
+ if (!['决策', 'decision', '选择', 'choice', '权衡', 'trade'].some(k => c.toLowerCase().includes(k))) add('info', 'DESIGN.md 建议记录设计决策和权衡', design);
84
+ }
85
+
86
+ return { modulePath, issues, structure };
87
+ }
88
+
89
+ function formatStructure(s, indent = 0) {
90
+ const pre = ' '.repeat(indent);
91
+ if (s.type === 'dir') {
92
+ const lines = [`${pre}\u{1F4C1} ${s.name}/`];
93
+ for (const ch of (s.children || [])) lines.push(formatStructure(ch, indent + 1));
94
+ return lines.join('\n');
95
+ }
96
+ const sz = (s.size || 0) < 1024 ? `(${s.size} B)` : `(${Math.floor(s.size / 1024)} KB)`;
97
+ return `${pre}\u{1F4C4} ${s.name} ${sz}`;
98
+ }
99
+
100
+ function formatReport(r, verbose) {
101
+ const passed = !r.issues.some(i => i.severity === 'error');
102
+ const errs = r.issues.filter(i => i.severity === 'error').length;
103
+ const warns = r.issues.filter(i => i.severity === 'warning').length;
104
+ const lines = [
105
+ '='.repeat(60), '模块完整性扫描报告', '='.repeat(60),
106
+ `\n模块路径: ${r.modulePath}`,
107
+ `扫描结果: ${passed ? '\u2713 通过' : '\u2717 未通过'}`,
108
+ `错误: ${errs} | 警告: ${warns}`
109
+ ];
110
+ if (r.issues.length) {
111
+ lines.push('\n' + '-'.repeat(40), '问题列表:', '-'.repeat(40));
112
+ for (const i of r.issues) {
113
+ const icon = { error: '\u2717', warning: '\u26A0', info: '\u2139' }[i.severity];
114
+ lines.push(` ${icon} [${i.severity.toUpperCase()}] ${i.message}`);
115
+ if (i.path && verbose) lines.push(` 路径: ${i.path}`);
116
+ }
117
+ }
118
+ if (verbose && r.structure.name) {
119
+ lines.push('\n' + '-'.repeat(40), '目录结构:', '-'.repeat(40), formatStructure(r.structure));
120
+ }
121
+ lines.push('\n' + '='.repeat(60));
122
+ return lines.join('\n');
123
+ }
124
+
125
+ // CLI
126
+ const args = process.argv.slice(2);
127
+ const verbose = args.includes('-v') || args.includes('--verbose');
128
+ const jsonOut = args.includes('--json');
129
+ const target = args.find(a => !a.startsWith('-')) || '.';
130
+
131
+ const result = scanModule(target);
132
+ const passed = !result.issues.some(i => i.severity === 'error');
133
+
134
+ if (jsonOut) {
135
+ console.log(JSON.stringify({
136
+ module_path: result.modulePath, passed,
137
+ error_count: result.issues.filter(i => i.severity === 'error').length,
138
+ warning_count: result.issues.filter(i => i.severity === 'warning').length,
139
+ issues: result.issues
140
+ }, null, 2));
141
+ } else {
142
+ console.log(formatReport(result, verbose));
143
+ }
144
+
145
+ process.exit(passed ? 0 : 1);
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: verify-quality
3
3
  description: 代码质量校验关卡。检测复杂度、重复代码、命名规范、函数长度等质量指标。当魔尊提到代码质量、复杂度检查、代码异味、重构建议、lint检查、代码规范时使用。在复杂模块、重构完成时自动触发。
4
+ license: MIT
5
+ compatibility: node>=18
4
6
  user-invocable: true
5
7
  disable-model-invocation: false
6
8
  allowed-tools: Bash, Read, Glob
@@ -24,9 +26,9 @@ argument-hint: <扫描路径>
24
26
 
25
27
  ```bash
26
28
  # 在 skill 目录下运行
27
- python scripts/quality_checker.py <扫描路径>
28
- python scripts/quality_checker.py <扫描路径> -v # 详细模式
29
- python scripts/quality_checker.py <扫描路径> --json # JSON 输出
29
+ node scripts/quality_checker.js <扫描路径>
30
+ node scripts/quality_checker.js <扫描路径> -v # 详细模式
31
+ node scripts/quality_checker.js <扫描路径> --json # JSON 输出
30
32
  ```
31
33
 
32
34
  ## 检测指标
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // 质量规则配置
8
+ const MAX_LINE_LENGTH = 120;
9
+ const MAX_FUNCTION_LENGTH = 50;
10
+ const MAX_FILE_LENGTH = 500;
11
+ const MAX_COMPLEXITY = 10;
12
+ const MAX_PARAMETERS = 5;
13
+ const MIN_FUNCTION_NAME_LENGTH = 2;
14
+
15
+ const EXCLUDE_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build', '.tox']);
16
+ const CODE_EXTENSIONS = new Set(['.py', '.js', '.ts', '.go', '.java', '.rs', '.c', '.cpp']);
17
+
18
+ const COMMENT_PREFIXES = { '.js': '//', '.ts': '//', '.go': '//', '.java': '//', '.c': '//', '.cpp': '//', '.rs': '//' };
19
+
20
+ // --- Analysis ---
21
+
22
+ function analyzeGenericFile(filePath) {
23
+ const metrics = { path: filePath, lines: 0, code_lines: 0, comment_lines: 0, blank_lines: 0, functions: 0, classes: 0, max_complexity: 0, avg_function_length: 0 };
24
+ const issues = [];
25
+ let content;
26
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return { metrics, issues }; }
27
+
28
+ const lines = content.split('\n');
29
+ metrics.lines = lines.length;
30
+ const prefix = COMMENT_PREFIXES[path.extname(filePath).toLowerCase()] || '//';
31
+
32
+ for (let i = 0; i < lines.length; i++) {
33
+ const stripped = lines[i].trim();
34
+ if (!stripped) metrics.blank_lines++;
35
+ else if (stripped.startsWith(prefix) || stripped.startsWith('/*') || stripped.startsWith('*')) metrics.comment_lines++;
36
+ else metrics.code_lines++;
37
+
38
+ if (lines[i].length > MAX_LINE_LENGTH) {
39
+ issues.push({ severity: 'info', category: '格式', message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`, file_path: filePath, line_number: i + 1, suggestion: null });
40
+ }
41
+ }
42
+
43
+ if (metrics.code_lines > MAX_FILE_LENGTH) {
44
+ issues.push({ severity: 'warning', category: '复杂度', message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`, file_path: filePath, suggestion: '考虑拆分为多个模块', line_number: null });
45
+ }
46
+
47
+ return { metrics, issues };
48
+ }
49
+
50
+ function analyzePythonFile(filePath) {
51
+ const metrics = { path: filePath, lines: 0, code_lines: 0, comment_lines: 0, blank_lines: 0, functions: 0, classes: 0, max_complexity: 0, avg_function_length: 0 };
52
+ const issues = [];
53
+ let content;
54
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch (e) {
55
+ issues.push({ severity: 'error', category: '文件', message: `无法读取文件: ${e.message}`, file_path: filePath, line_number: null, suggestion: null });
56
+ return { metrics, issues };
57
+ }
58
+
59
+ const lines = content.split('\n');
60
+ metrics.lines = lines.length;
61
+ let inMultiline = false;
62
+
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const stripped = lines[i].trim();
65
+ if (!stripped) { metrics.blank_lines++; }
66
+ else if (stripped.startsWith('#')) { metrics.comment_lines++; }
67
+ else if (stripped.includes('"""') || stripped.includes("'''")) {
68
+ const dq = (stripped.match(/"""/g) || []).length;
69
+ const sq = (stripped.match(/'''/g) || []).length;
70
+ if (dq === 2 || sq === 2) { metrics.comment_lines++; }
71
+ else { inMultiline = !inMultiline; metrics.comment_lines++; }
72
+ } else if (inMultiline) { metrics.comment_lines++; }
73
+ else { metrics.code_lines++; }
74
+
75
+ if (lines[i].length > MAX_LINE_LENGTH) {
76
+ issues.push({ severity: 'info', category: '格式', message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`, file_path: filePath, line_number: i + 1, suggestion: null });
77
+ }
78
+ }
79
+
80
+ if (metrics.code_lines > MAX_FILE_LENGTH) {
81
+ issues.push({ severity: 'warning', category: '复杂度', message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`, file_path: filePath, suggestion: '考虑拆分为多个模块', line_number: null });
82
+ }
83
+
84
+ // Regex-based Python analysis (no AST available in Node)
85
+ const funcRegex = /^( *)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gm;
86
+ const classRegex = /^( *)class\s+(\w+)/gm;
87
+ const functions = [];
88
+ let match;
89
+
90
+ while ((match = funcRegex.exec(content)) !== null) {
91
+ const lineNum = content.substring(0, match.index).split('\n').length;
92
+ const name = match[2];
93
+ const indent = match[1].length;
94
+ const params = match[3].trim() ? match[3].split(',').map(p => p.trim()).filter(p => p && p !== 'self' && p !== 'cls') : [];
95
+
96
+ // Calculate function length by finding next line at same or lesser indent
97
+ const funcLines = lines.slice(lineNum); // lines after def
98
+ let length = 1;
99
+ for (let j = 1; j < funcLines.length; j++) {
100
+ const l = funcLines[j];
101
+ if (l.trim() === '') { length++; continue; }
102
+ const curIndent = l.match(/^(\s*)/)[1].length;
103
+ if (curIndent <= indent && l.trim() !== '') break;
104
+ length++;
105
+ }
106
+
107
+ // Estimate complexity from function body
108
+ const bodyLines = lines.slice(lineNum, lineNum + length - 1);
109
+ let complexity = 1;
110
+ for (const bl of bodyLines) {
111
+ const s = bl.trim();
112
+ if (/^(if|elif|while|for)\s/.test(s) || /^(if|elif|while|for)\(/.test(s)) complexity++;
113
+ if (/^except(\s|:)/.test(s)) complexity++;
114
+ if (/\s(and|or)\s/.test(s)) complexity++;
115
+ if (/\sfor\s/.test(s) && /\sin\s/.test(s) && (s.includes('[') || s.includes('('))) complexity++;
116
+ }
117
+
118
+ functions.push({ name, line: lineNum, length, complexity, parameters: params.length });
119
+ metrics.max_complexity = Math.max(metrics.max_complexity, complexity);
120
+
121
+ // Check function length
122
+ if (length > MAX_FUNCTION_LENGTH) {
123
+ issues.push({ severity: 'warning', category: '复杂度', message: `函数 '${name}' 过长 (${length} 行 > ${MAX_FUNCTION_LENGTH})`, file_path: filePath, line_number: lineNum, suggestion: '考虑拆分为多个小函数' });
124
+ }
125
+ // Check complexity
126
+ if (complexity > MAX_COMPLEXITY) {
127
+ issues.push({ severity: 'warning', category: '复杂度', message: `函数 '${name}' 圈复杂度过高 (${complexity} > ${MAX_COMPLEXITY})`, file_path: filePath, line_number: lineNum, suggestion: '减少嵌套层级,提取子函数' });
128
+ }
129
+ // Check parameter count
130
+ if (params.length > MAX_PARAMETERS) {
131
+ issues.push({ severity: 'warning', category: '设计', message: `函数 '${name}' 参数过多 (${params.length} > ${MAX_PARAMETERS})`, file_path: filePath, line_number: lineNum, suggestion: '考虑使用配置对象或数据类封装参数' });
132
+ }
133
+ // Check naming
134
+ const SPECIAL = new Set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass', 'setUpModule', 'tearDownModule']);
135
+ if (!name.startsWith('_') && !SPECIAL.has(name) && !name.startsWith('visit_')) {
136
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
137
+ issues.push({ severity: 'info', category: '命名', message: `函数名 '${name}' 不符合 snake_case 规范`, file_path: filePath, line_number: lineNum, suggestion: '函数名应使用 snake_case,如 my_function_name' });
138
+ }
139
+ }
140
+ if (name.length < MIN_FUNCTION_NAME_LENGTH) {
141
+ issues.push({ severity: 'warning', category: '命名', message: `函数名 '${name}' 过短`, file_path: filePath, line_number: lineNum, suggestion: '使用更具描述性的函数名' });
142
+ }
143
+ }
144
+
145
+ while ((match = classRegex.exec(content)) !== null) {
146
+ const lineNum = content.substring(0, match.index).split('\n').length;
147
+ const name = match[2];
148
+ metrics.classes++;
149
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
150
+ issues.push({ severity: 'warning', category: '命名', message: `类名 '${name}' 不符合 PascalCase 规范`, file_path: filePath, line_number: lineNum, suggestion: '类名应使用 PascalCase,如 MyClassName' });
151
+ }
152
+ }
153
+
154
+ metrics.functions = functions.length;
155
+ if (functions.length > 0) {
156
+ metrics.avg_function_length = functions.reduce((s, f) => s + f.length, 0) / functions.length;
157
+ }
158
+
159
+ return { metrics, issues };
160
+ }
161
+
162
+ // --- Directory scan ---
163
+
164
+ function scanDirectory(scanPath, excludeDirs) {
165
+ const resolved = path.resolve(scanPath);
166
+ const exclude = excludeDirs || EXCLUDE_DIRS;
167
+ const result = { scan_path: resolved, files_scanned: 0, total_lines: 0, total_code_lines: 0, issues: [], file_metrics: [] };
168
+
169
+ function walk(dir) {
170
+ let entries;
171
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
172
+ for (const entry of entries) {
173
+ if (exclude.has(entry.name)) continue;
174
+ const full = path.join(dir, entry.name);
175
+ if (entry.isDirectory()) { walk(full); continue; }
176
+ const ext = path.extname(entry.name).toLowerCase();
177
+ if (!CODE_EXTENSIONS.has(ext)) continue;
178
+
179
+ result.files_scanned++;
180
+ const { metrics, issues } = ext === '.py' ? analyzePythonFile(full) : analyzeGenericFile(full);
181
+ result.file_metrics.push(metrics);
182
+ result.issues.push(...issues);
183
+ result.total_lines += metrics.lines;
184
+ result.total_code_lines += metrics.code_lines;
185
+ }
186
+ }
187
+
188
+ walk(resolved);
189
+ return result;
190
+ }
191
+
192
+ // --- Reporting ---
193
+
194
+ function errorCount(result) { return result.issues.filter(i => i.severity === 'error').length; }
195
+ function warningCount(result) { return result.issues.filter(i => i.severity === 'warning').length; }
196
+ function passed(result) { return !result.issues.some(i => i.severity === 'error'); }
197
+
198
+ function formatReport(result, verbose) {
199
+ const lines = [];
200
+ const sep = '='.repeat(60);
201
+ const dash = '-'.repeat(40);
202
+ lines.push(sep, '代码质量检查报告', sep);
203
+ lines.push(`\n扫描路径: ${result.scan_path}`);
204
+ lines.push(`扫描文件: ${result.files_scanned}`);
205
+ lines.push(`总行数: ${result.total_lines}`);
206
+ lines.push(`代码行数: ${result.total_code_lines}`);
207
+ lines.push(`检查结果: ${passed(result) ? '✓ 通过' : '✗ 需要关注'}`);
208
+ lines.push(`错误: ${errorCount(result)} | 警告: ${warningCount(result)}`);
209
+
210
+ if (result.issues.length) {
211
+ lines.push('\n' + dash, '问题列表:', dash);
212
+ const byCategory = {};
213
+ for (const issue of result.issues) {
214
+ (byCategory[issue.category] || (byCategory[issue.category] = [])).push(issue);
215
+ }
216
+ const icons = { error: '✗', warning: '⚠', info: 'ℹ' };
217
+ for (const cat of Object.keys(byCategory).sort()) {
218
+ const catIssues = byCategory[cat];
219
+ lines.push(`\n【${cat}】(${catIssues.length} 个)`);
220
+ for (const issue of catIssues.slice(0, 10)) {
221
+ const icon = icons[issue.severity];
222
+ const loc = issue.line_number ? `:${issue.line_number}` : '';
223
+ lines.push(` ${icon} ${issue.file_path}${loc}`);
224
+ lines.push(` ${issue.message}`);
225
+ if (verbose && issue.suggestion) lines.push(` 💡 ${issue.suggestion}`);
226
+ }
227
+ if (catIssues.length > 10) lines.push(` ... 及其他 ${catIssues.length - 10} 个问题`);
228
+ }
229
+ }
230
+
231
+ if (verbose && result.file_metrics.length) {
232
+ const complex = result.file_metrics.filter(m => m.max_complexity > 0).sort((a, b) => b.max_complexity - a.max_complexity).slice(0, 5);
233
+ if (complex.length) {
234
+ lines.push('\n' + dash, '复杂度最高的文件:', dash);
235
+ for (const m of complex) lines.push(` ${m.path}: 复杂度 ${m.max_complexity}, ${m.functions} 个函数`);
236
+ }
237
+ }
238
+
239
+ lines.push('\n' + sep);
240
+ return lines.join('\n');
241
+ }
242
+
243
+ // --- CLI ---
244
+
245
+ function main() {
246
+ const args = process.argv.slice(2);
247
+ let scanPath = '.', verbose = false, jsonOutput = false;
248
+
249
+ for (let i = 0; i < args.length; i++) {
250
+ if (args[i] === '-v' || args[i] === '--verbose') verbose = true;
251
+ else if (args[i] === '--json') jsonOutput = true;
252
+ else if (!args[i].startsWith('-')) scanPath = args[i];
253
+ }
254
+
255
+ const result = scanDirectory(scanPath);
256
+
257
+ if (jsonOutput) {
258
+ const output = {
259
+ scan_path: result.scan_path,
260
+ files_scanned: result.files_scanned,
261
+ total_lines: result.total_lines,
262
+ total_code_lines: result.total_code_lines,
263
+ passed: passed(result),
264
+ error_count: errorCount(result),
265
+ warning_count: warningCount(result),
266
+ issues: result.issues
267
+ };
268
+ console.log(JSON.stringify(output, null, 2));
269
+ } else {
270
+ console.log(formatReport(result, verbose));
271
+ }
272
+
273
+ process.exit(passed(result) ? 0 : 1);
274
+ }
275
+
276
+ main();
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: verify-security
3
3
  description: 安全校验关卡。自动扫描代码安全漏洞,检测危险模式,确保安全决策有文档记录。当魔尊提到安全扫描、漏洞检测、安全审计、代码安全、OWASP、注入检测、敏感信息泄露时使用。在新建模块、安全相关变更、攻防任务、重构完成时自动触发。
4
+ license: MIT
5
+ compatibility: node>=18
4
6
  user-invocable: true
5
7
  disable-model-invocation: false
6
8
  allowed-tools: Bash, Read, Grep
@@ -24,10 +26,10 @@ Critical/High 问题必须修复后才能交付
24
26
 
25
27
  ```bash
26
28
  # 在 skill 目录下运行
27
- python scripts/security_scanner.py <扫描路径>
28
- python scripts/security_scanner.py <扫描路径> -v # 详细模式
29
- python scripts/security_scanner.py <扫描路径> --json # JSON 输出
30
- python scripts/security_scanner.py <扫描路径> --exclude vendor # 排除目录
29
+ node scripts/security_scanner.js <扫描路径>
30
+ node scripts/security_scanner.js <扫描路径> -v # 详细模式
31
+ node scripts/security_scanner.js <扫描路径> --json # JSON 输出
32
+ node scripts/security_scanner.js <扫描路径> --exclude vendor # 排除目录
31
33
  ```
32
34
 
33
35
  ## 检测范围
@@ -98,7 +100,7 @@ html/template 自动转义
98
100
  ## 校验流程
99
101
 
100
102
  ```
101
- 1. 运行 security_scanner.py 自动扫描
103
+ 1. 运行 security_scanner.js 自动扫描
102
104
  2. 分析扫描结果,按严重度排序
103
105
  3. 检查安全决策是否有文档记录
104
106
  4. 输出安全校验报告
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
8
+
9
+ const SECURITY_RULES = [
10
+ { id: 'SQL_INJECTION_DYNAMIC', category: '注入', severity: 'critical', pattern: /\b(execute|query|raw)\s*\(\s*(f["']|["'][^"'\n]*["']\s*\+\s*|["'][^"'\n]*["']\s*%\s*[^,)]|["'][^"'\n]*["']\.format\s*\()/i, extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'], message: '可能存在 SQL 注入风险', recommendation: '使用参数化查询或 ORM' },
11
+ { id: 'SQL_INJECTION_FSTRING', category: '注入', severity: 'critical', pattern: /cursor\.(execute|executemany)\s*\(\s*f["']/i, extensions: ['.py'], message: '使用 f-string 构造 SQL 语句', recommendation: "使用参数化查询: cursor.execute('SELECT * FROM t WHERE id = %s', (id,))" },
12
+ { id: 'COMMAND_INJECTION', category: '注入', severity: 'critical', pattern: /(os\.system|os\.popen|subprocess\.call|subprocess\.run|subprocess\.Popen)\s*\([^)]*shell\s*=\s*True/i, extensions: ['.py'], message: '使用 shell=True 可能导致命令注入', recommendation: '避免 shell=True,使用列表参数' },
13
+ { id: 'COMMAND_INJECTION_EVAL', category: '注入', severity: 'critical', pattern: /\b(eval|exec)\s*\([^)]*\b(input|request|argv|args)/i, extensions: ['.py'], message: 'eval/exec 执行用户输入', recommendation: '避免对用户输入使用 eval/exec' },
14
+ { id: 'HARDCODED_SECRET', category: '敏感信息', severity: 'high', pattern: /(?<!\w)(password|passwd|pwd|secret|api_key|apikey|token|auth_token)\s*=\s*["'][^"']{8,}["']/i, excludePattern: /(example|placeholder|changeme|xxx|your[_-]|TODO|FIXME|<.*>|\*{3,})/i, extensions: ['.py', '.js', '.ts', '.go', '.java', '.php', '.rb', '.yaml', '.yml', '.json', '.env'], message: '可能存在硬编码密钥/密码', recommendation: '使用环境变量或密钥管理服务' },
15
+ { id: 'HARDCODED_AWS_KEY', category: '敏感信息', severity: 'critical', pattern: /AKIA[0-9A-Z]{16}/, extensions: ['*'], message: '发现 AWS Access Key', recommendation: '立即轮换密钥,使用 IAM 角色或环境变量' },
16
+ { id: 'HARDCODED_PRIVATE_KEY', category: '敏感信息', severity: 'critical', pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, extensions: ['*'], message: '发现私钥', recommendation: '私钥不应提交到代码库' },
17
+ { id: 'XSS_INNERHTML', category: 'XSS', severity: 'high', pattern: /\.innerHTML\s*=|\.outerHTML\s*=|document\.write\s*\(/i, extensions: ['.js', '.ts', '.jsx', '.tsx', '.html'], message: '直接操作 innerHTML 可能导致 XSS', recommendation: '使用 textContent 或框架的安全绑定' },
18
+ { id: 'XSS_DANGEROUSLY', category: 'XSS', severity: 'medium', pattern: /dangerouslySetInnerHTML/i, extensions: ['.js', '.ts', '.jsx', '.tsx'], message: '使用 dangerouslySetInnerHTML', recommendation: '确保内容已经过净化处理' },
19
+ { id: 'UNSAFE_PICKLE', category: '反序列化', severity: 'high', pattern: /pickle\.loads?\s*\(|yaml\.load\s*\([^)]*Loader\s*=\s*yaml\.Loader/i, extensions: ['.py'], message: '不安全的反序列化', recommendation: '使用 yaml.safe_load() 或验证数据来源' },
20
+ { id: 'WEAK_CRYPTO_MD5', category: '加密', severity: 'medium', pattern: /\b(md5|MD5)\s*\(|hashlib\.md5\s*\(/i, extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'], message: '使用弱哈希算法 MD5', recommendation: '密码存储使用 bcrypt/argon2,完整性校验使用 SHA-256+' },
21
+ { id: 'WEAK_CRYPTO_SHA1', category: '加密', severity: 'low', pattern: /\b(sha1|SHA1)\s*\(|hashlib\.sha1\s*\(/i, extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'], message: '使用弱哈希算法 SHA1', recommendation: '使用 SHA-256 或更强的算法' },
22
+ { id: 'PATH_TRAVERSAL', category: '路径遍历', severity: 'high', pattern: /(open|read|write|Path|os\.path\.join)\s*\([^\n]*(request|input|argv|args|params|query|form|path_param)\b/i, extensions: ['.py'], message: '可能存在路径遍历风险', recommendation: '验证并规范化用户输入的路径' },
23
+ { id: 'SSRF', category: 'SSRF', severity: 'high', pattern: /(requests\.(get|post|put|delete|head)|urllib\.request\.urlopen)\s*\([^\n]*(request|input|argv|args|params|query|url)\b/i, extensions: ['.py'], message: '可能存在 SSRF 风险', recommendation: '验证并限制目标 URL' },
24
+ { id: 'DEBUG_CODE', category: '调试', severity: 'low', pattern: /\b(console\.log|debugger|pdb\.set_trace|breakpoint)\s*\(/i, extensions: ['.py', '.js', '.ts'], message: '发现调试代码', recommendation: '生产环境移除调试代码' },
25
+ { id: 'INSECURE_RANDOM', category: '加密', severity: 'medium', pattern: /\brandom\.(random|randint|choice|shuffle)\s*\(/i, extensions: ['.py'], message: '使用不安全的随机数生成器', recommendation: '安全场景使用 secrets 模块' },
26
+ { id: 'XXE', category: 'XXE', severity: 'high', pattern: /etree\.(parse|fromstring)\s*\([^)]*\)|xml\.dom\.minidom\.parse/i, extensions: ['.py'], message: 'XML 解析可能存在 XXE 风险', recommendation: '禁用外部实体: parser = etree.XMLParser(resolve_entities=False)' },
27
+ ];
28
+
29
+ const CODE_EXTENSIONS = new Set(['.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.java', '.php', '.rb', '.yaml', '.yml', '.json']);
30
+ const DEFAULT_EXCLUDES = ['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build', '.tox', 'tests', 'test', '__tests__', 'spec'];
31
+
32
+ function scanFile(filePath, rules) {
33
+ const findings = [];
34
+ const ext = path.extname(filePath).toLowerCase();
35
+ let content;
36
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return findings; }
37
+ const lines = content.split('\n');
38
+
39
+ for (const rule of rules) {
40
+ const exts = rule.extensions;
41
+ if (!exts.includes('*') && !exts.includes(ext)) continue;
42
+
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i];
45
+ const stripped = line.trim();
46
+ if (stripped.startsWith('#') || stripped.startsWith('//') || stripped.startsWith('*') || stripped.startsWith('/*')) continue;
47
+
48
+ if (rule.pattern.test(line)) {
49
+ rule.pattern.lastIndex = 0;
50
+ if (rule.excludePattern && rule.excludePattern.test(line)) { rule.excludePattern.lastIndex = 0; continue; }
51
+ findings.push({ severity: rule.severity, category: rule.category, message: rule.message, file_path: filePath, line_number: i + 1, line_content: stripped.slice(0, 100), recommendation: rule.recommendation });
52
+ }
53
+ }
54
+ }
55
+ return findings;
56
+ }
57
+
58
+ function walkDir(dir, excludeDirs) {
59
+ const results = [];
60
+ let entries;
61
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
62
+ for (const entry of entries) {
63
+ if (excludeDirs.includes(entry.name)) continue;
64
+ const full = path.join(dir, entry.name);
65
+ if (entry.isDirectory()) { results.push(...walkDir(full, excludeDirs)); }
66
+ else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) { results.push(full); }
67
+ }
68
+ return results;
69
+ }
70
+
71
+ function scanDirectory(scanPath, excludeDirs) {
72
+ const resolved = path.resolve(scanPath);
73
+ const findings = [];
74
+ const files = walkDir(resolved, excludeDirs);
75
+ for (const f of files) findings.push(...scanFile(f, SECURITY_RULES));
76
+ findings.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
77
+ const passed = !findings.some(f => f.severity === 'critical' || f.severity === 'high');
78
+ return { scan_path: resolved, files_scanned: files.length, passed, findings };
79
+ }
80
+
81
+ function countBySeverity(findings) {
82
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
83
+ for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
84
+ return counts;
85
+ }
86
+
87
+ function formatReport(result, verbose) {
88
+ const counts = countBySeverity(result.findings);
89
+ const icons = { critical: '\u{1F534}', high: '\u{1F7E0}', medium: '\u{1F7E1}', low: '\u{1F535}', info: '\u26AA' };
90
+ const lines = [
91
+ '='.repeat(60), '代码安全扫描报告', '='.repeat(60),
92
+ `\n扫描路径: ${result.scan_path}`, `扫描文件: ${result.files_scanned}`,
93
+ `扫描结果: ${result.passed ? '\u2713 通过' : '\u2717 发现高危问题'}`,
94
+ `\n严重: ${counts.critical} | 高危: ${counts.high} | 中危: ${counts.medium} | 低危: ${counts.low}`,
95
+ ];
96
+ if (result.findings.length) {
97
+ lines.push('\n' + '-'.repeat(40), '发现问题:', '-'.repeat(40));
98
+ for (const f of result.findings) {
99
+ lines.push(`\n${icons[f.severity] || ''} [${f.severity.toUpperCase()}] ${f.category}`);
100
+ lines.push(` 文件: ${f.file_path}:${f.line_number}`);
101
+ lines.push(` 问题: ${f.message}`);
102
+ if (verbose) lines.push(` 代码: ${f.line_content}`);
103
+ lines.push(` 建议: ${f.recommendation}`);
104
+ }
105
+ }
106
+ lines.push('\n' + '='.repeat(60));
107
+ return lines.join('\n');
108
+ }
109
+
110
+ function main() {
111
+ const args = process.argv.slice(2);
112
+ let scanPath = '.', verbose = false, jsonOut = false, extraExcludes = [];
113
+
114
+ for (let i = 0; i < args.length; i++) {
115
+ if (args[i] === '-v' || args[i] === '--verbose') verbose = true;
116
+ else if (args[i] === '--json') jsonOut = true;
117
+ else if (args[i] === '--exclude') { while (i + 1 < args.length && !args[i + 1].startsWith('-')) extraExcludes.push(args[++i]); }
118
+ else if (args[i] === '--help' || args[i] === '-h') { console.log('Usage: security_scanner.js [path] [-v] [--json] [--exclude dir1 dir2]'); process.exit(0); }
119
+ else if (!args[i].startsWith('-')) scanPath = args[i];
120
+ }
121
+
122
+ const excludeDirs = [...DEFAULT_EXCLUDES, ...extraExcludes];
123
+ const result = scanDirectory(scanPath, excludeDirs);
124
+
125
+ if (jsonOut) {
126
+ console.log(JSON.stringify({ scan_path: result.scan_path, files_scanned: result.files_scanned, passed: result.passed, counts: countBySeverity(result.findings), findings: result.findings }, null, 2));
127
+ } else {
128
+ console.log(formatReport(result, verbose));
129
+ }
130
+ process.exit(result.passed ? 0 : 1);
131
+ }
132
+
133
+ main();