code-abyss 1.6.16 → 1.7.1

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 (97) hide show
  1. package/README.md +8 -6
  2. package/bin/install.js +59 -163
  3. package/bin/lib/ccline.js +82 -0
  4. package/bin/lib/utils.js +61 -0
  5. package/package.json +5 -2
  6. package/skills/SKILL.md +24 -16
  7. package/skills/domains/ai/SKILL.md +2 -2
  8. package/skills/domains/ai/prompt-and-eval.md +279 -0
  9. package/skills/domains/architecture/SKILL.md +2 -3
  10. package/skills/domains/architecture/security-arch.md +87 -0
  11. package/skills/domains/data-engineering/SKILL.md +188 -26
  12. package/skills/domains/development/SKILL.md +1 -4
  13. package/skills/domains/devops/SKILL.md +3 -5
  14. package/skills/domains/devops/performance.md +63 -0
  15. package/skills/domains/devops/testing.md +97 -0
  16. package/skills/domains/frontend-design/SKILL.md +12 -3
  17. package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
  18. package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
  19. package/skills/domains/frontend-design/engineering.md +287 -0
  20. package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
  21. package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
  22. package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
  23. package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
  24. package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
  25. package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
  26. package/skills/domains/infrastructure/SKILL.md +174 -34
  27. package/skills/domains/mobile/SKILL.md +211 -21
  28. package/skills/domains/orchestration/SKILL.md +1 -0
  29. package/skills/domains/security/SKILL.md +4 -6
  30. package/skills/domains/security/blue-team.md +57 -0
  31. package/skills/domains/security/red-team.md +54 -0
  32. package/skills/domains/security/threat-intel.md +50 -0
  33. package/skills/orchestration/multi-agent/SKILL.md +195 -46
  34. package/skills/run_skill.js +139 -0
  35. package/skills/tools/gen-docs/SKILL.md +6 -4
  36. package/skills/tools/gen-docs/scripts/doc_generator.js +363 -0
  37. package/skills/tools/lib/shared.js +98 -0
  38. package/skills/tools/verify-change/SKILL.md +8 -6
  39. package/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
  40. package/skills/tools/verify-module/SKILL.md +6 -4
  41. package/skills/tools/verify-module/scripts/module_scanner.js +171 -0
  42. package/skills/tools/verify-quality/SKILL.md +5 -3
  43. package/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
  44. package/skills/tools/verify-security/SKILL.md +7 -5
  45. package/skills/tools/verify-security/scripts/security_scanner.js +283 -0
  46. package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
  47. package/skills/domains/COVERAGE_PLAN.md +0 -232
  48. package/skills/domains/ai/model-evaluation.md +0 -790
  49. package/skills/domains/ai/prompt-engineering.md +0 -703
  50. package/skills/domains/architecture/compliance.md +0 -299
  51. package/skills/domains/architecture/data-security.md +0 -184
  52. package/skills/domains/data-engineering/data-pipeline.md +0 -762
  53. package/skills/domains/data-engineering/data-quality.md +0 -894
  54. package/skills/domains/data-engineering/stream-processing.md +0 -791
  55. package/skills/domains/development/dart.md +0 -963
  56. package/skills/domains/development/kotlin.md +0 -834
  57. package/skills/domains/development/php.md +0 -659
  58. package/skills/domains/development/swift.md +0 -755
  59. package/skills/domains/devops/e2e-testing.md +0 -914
  60. package/skills/domains/devops/performance-testing.md +0 -734
  61. package/skills/domains/devops/testing-strategy.md +0 -667
  62. package/skills/domains/frontend-design/build-tools.md +0 -743
  63. package/skills/domains/frontend-design/performance.md +0 -734
  64. package/skills/domains/frontend-design/testing.md +0 -699
  65. package/skills/domains/infrastructure/gitops.md +0 -735
  66. package/skills/domains/infrastructure/iac.md +0 -855
  67. package/skills/domains/infrastructure/kubernetes.md +0 -1018
  68. package/skills/domains/mobile/android-dev.md +0 -979
  69. package/skills/domains/mobile/cross-platform.md +0 -795
  70. package/skills/domains/mobile/ios-dev.md +0 -931
  71. package/skills/domains/security/secrets-management.md +0 -834
  72. package/skills/domains/security/supply-chain.md +0 -931
  73. package/skills/domains/security/threat-modeling.md +0 -828
  74. package/skills/run_skill.py +0 -153
  75. package/skills/tests/README.md +0 -225
  76. package/skills/tests/SUMMARY.md +0 -362
  77. package/skills/tests/__init__.py +0 -3
  78. package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
  79. package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
  80. package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
  81. package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
  82. package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
  83. package/skills/tests/test_change_analyzer.py +0 -558
  84. package/skills/tests/test_doc_generator.py +0 -538
  85. package/skills/tests/test_module_scanner.py +0 -376
  86. package/skills/tests/test_quality_checker.py +0 -516
  87. package/skills/tests/test_security_scanner.py +0 -426
  88. package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
  89. package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
  90. package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
  91. package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
  92. package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
  93. package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
  94. package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
  95. package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
  96. package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
  97. package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
7
+
8
+ // 质量规则配置
9
+ const MAX_LINE_LENGTH = 120;
10
+ const MAX_FUNCTION_LENGTH = 50;
11
+ const MAX_FILE_LENGTH = 500;
12
+ const MAX_COMPLEXITY = 10;
13
+ const MAX_PARAMETERS = 5;
14
+ const MIN_FUNCTION_NAME_LENGTH = 2;
15
+
16
+ const EXCLUDE_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build', '.tox']);
17
+ const CODE_EXTENSIONS = new Set(['.py', '.js', '.ts', '.go', '.java', '.rs', '.c', '.cpp']);
18
+
19
+ const COMMENT_PREFIXES = {
20
+ '.js': '//', '.ts': '//', '.go': '//', '.java': '//',
21
+ '.c': '//', '.cpp': '//', '.rs': '//',
22
+ };
23
+
24
+ // --- Analysis ---
25
+
26
+ function analyzeGenericFile(filePath) {
27
+ const metrics = {
28
+ path: filePath, lines: 0, code_lines: 0, comment_lines: 0,
29
+ blank_lines: 0, functions: 0, classes: 0,
30
+ max_complexity: 0, avg_function_length: 0,
31
+ };
32
+ const issues = [];
33
+ let content;
34
+ try {
35
+ content = fs.readFileSync(filePath, 'utf-8');
36
+ } catch { return { metrics, issues }; }
37
+
38
+ const lines = content.split('\n');
39
+ metrics.lines = lines.length;
40
+ const prefix = COMMENT_PREFIXES[
41
+ path.extname(filePath).toLowerCase()
42
+ ] || '//';
43
+
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const stripped = lines[i].trim();
46
+ if (!stripped) metrics.blank_lines++;
47
+ else if (
48
+ stripped.startsWith(prefix) ||
49
+ stripped.startsWith('/*') ||
50
+ stripped.startsWith('*')
51
+ ) metrics.comment_lines++;
52
+ else metrics.code_lines++;
53
+
54
+ if (lines[i].length > MAX_LINE_LENGTH) {
55
+ issues.push({
56
+ severity: 'info', category: '格式',
57
+ message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`,
58
+ file_path: filePath, line_number: i + 1,
59
+ suggestion: null,
60
+ });
61
+ }
62
+ }
63
+
64
+ if (metrics.code_lines > MAX_FILE_LENGTH) {
65
+ issues.push({
66
+ severity: 'warning', category: '复杂度',
67
+ message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`,
68
+ file_path: filePath, suggestion: '考虑拆分为多个模块',
69
+ line_number: null,
70
+ });
71
+ }
72
+
73
+ return { metrics, issues };
74
+ }
75
+
76
+ function analyzePythonFile(filePath) {
77
+ const metrics = {
78
+ path: filePath, lines: 0, code_lines: 0, comment_lines: 0,
79
+ blank_lines: 0, functions: 0, classes: 0,
80
+ max_complexity: 0, avg_function_length: 0,
81
+ };
82
+ const issues = [];
83
+ let content;
84
+ try {
85
+ content = fs.readFileSync(filePath, 'utf-8');
86
+ } catch (e) {
87
+ issues.push({
88
+ severity: 'error', category: '文件',
89
+ message: `无法读取文件: ${e.message}`,
90
+ file_path: filePath, line_number: null, suggestion: null,
91
+ });
92
+ return { metrics, issues };
93
+ }
94
+
95
+ const lines = content.split('\n');
96
+ metrics.lines = lines.length;
97
+ let inMultiline = false;
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const stripped = lines[i].trim();
101
+ if (!stripped) { metrics.blank_lines++; }
102
+ else if (stripped.startsWith('#')) { metrics.comment_lines++; }
103
+ else if (stripped.includes('"""') || stripped.includes("'''")) {
104
+ const dq = (stripped.match(/"""/g) || []).length;
105
+ const sq = (stripped.match(/'''/g) || []).length;
106
+ if (dq === 2 || sq === 2) { metrics.comment_lines++; }
107
+ else { inMultiline = !inMultiline; metrics.comment_lines++; }
108
+ } else if (inMultiline) { metrics.comment_lines++; }
109
+ else { metrics.code_lines++; }
110
+
111
+ if (lines[i].length > MAX_LINE_LENGTH) {
112
+ issues.push({
113
+ severity: 'info', category: '格式',
114
+ message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`,
115
+ file_path: filePath, line_number: i + 1,
116
+ suggestion: null,
117
+ });
118
+ }
119
+ }
120
+
121
+ if (metrics.code_lines > MAX_FILE_LENGTH) {
122
+ issues.push({
123
+ severity: 'warning', category: '复杂度',
124
+ message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`,
125
+ file_path: filePath, suggestion: '考虑拆分为多个模块',
126
+ line_number: null,
127
+ });
128
+ }
129
+
130
+ // Regex-based Python analysis (no AST available in Node)
131
+ const funcRegex = /^( *)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gm;
132
+ const classRegex = /^( *)class\s+(\w+)/gm;
133
+ const functions = [];
134
+ let match;
135
+
136
+ while ((match = funcRegex.exec(content)) !== null) {
137
+ const lineNum = content.substring(0, match.index).split('\n').length;
138
+ const name = match[2];
139
+ const indent = match[1].length;
140
+ const params = match[3].trim()
141
+ ? match[3].split(',').map(p => p.trim())
142
+ .filter(p => p && p !== 'self' && p !== 'cls')
143
+ : [];
144
+
145
+ // Calculate function length by finding next line at same or lesser indent
146
+ const funcLines = lines.slice(lineNum); // lines after def
147
+ let length = 1;
148
+ for (let j = 1; j < funcLines.length; j++) {
149
+ const l = funcLines[j];
150
+ if (l.trim() === '') { length++; continue; }
151
+ const curIndent = l.match(/^(\s*)/)[1].length;
152
+ if (curIndent <= indent && l.trim() !== '') break;
153
+ length++;
154
+ }
155
+
156
+ // Estimate complexity from function body
157
+ const bodyLines = lines.slice(lineNum, lineNum + length - 1);
158
+ let complexity = 1;
159
+ for (const bl of bodyLines) {
160
+ const s = bl.trim();
161
+ if (/^(if|elif|while|for)\s/.test(s) || /^(if|elif|while|for)\(/.test(s)) complexity++;
162
+ if (/^except(\s|:)/.test(s)) complexity++;
163
+ if (/\s(and|or)\s/.test(s)) complexity++;
164
+ if (/\sfor\s/.test(s) && /\sin\s/.test(s) && (s.includes('[') || s.includes('('))) complexity++;
165
+ }
166
+
167
+ functions.push({ name, line: lineNum, length, complexity, parameters: params.length });
168
+ metrics.max_complexity = Math.max(metrics.max_complexity, complexity);
169
+
170
+ // Check function length
171
+ if (length > MAX_FUNCTION_LENGTH) {
172
+ issues.push({
173
+ severity: 'warning', category: '复杂度',
174
+ message: `函数 '${name}' 过长 (${length} 行 > ${MAX_FUNCTION_LENGTH})`,
175
+ file_path: filePath, line_number: lineNum,
176
+ suggestion: '考虑拆分为多个小函数',
177
+ });
178
+ }
179
+ // Check complexity
180
+ if (complexity > MAX_COMPLEXITY) {
181
+ issues.push({
182
+ severity: 'warning', category: '复杂度',
183
+ message: `函数 '${name}' 圈复杂度过高 (${complexity} > ${MAX_COMPLEXITY})`,
184
+ file_path: filePath, line_number: lineNum,
185
+ suggestion: '减少嵌套层级,提取子函数',
186
+ });
187
+ }
188
+ // Check parameter count
189
+ if (params.length > MAX_PARAMETERS) {
190
+ issues.push({
191
+ severity: 'warning', category: '设计',
192
+ message: `函数 '${name}' 参数过多 (${params.length} > ${MAX_PARAMETERS})`,
193
+ file_path: filePath, line_number: lineNum,
194
+ suggestion: '考虑使用配置对象或数据类封装参数',
195
+ });
196
+ }
197
+ // Check naming
198
+ const SPECIAL = new Set([
199
+ 'setUp', 'tearDown', 'setUpClass',
200
+ 'tearDownClass', 'setUpModule', 'tearDownModule',
201
+ ]);
202
+ if (!name.startsWith('_') && !SPECIAL.has(name) && !name.startsWith('visit_')) {
203
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
204
+ issues.push({
205
+ severity: 'info', category: '命名',
206
+ message: `函数名 '${name}' 不符合 snake_case 规范`,
207
+ file_path: filePath, line_number: lineNum,
208
+ suggestion: '函数名应使用 snake_case',
209
+ });
210
+ }
211
+ }
212
+ if (name.length < MIN_FUNCTION_NAME_LENGTH) {
213
+ issues.push({
214
+ severity: 'warning', category: '命名',
215
+ message: `函数名 '${name}' 过短`,
216
+ file_path: filePath, line_number: lineNum,
217
+ suggestion: '使用更具描述性的函数名',
218
+ });
219
+ }
220
+ }
221
+
222
+ while ((match = classRegex.exec(content)) !== null) {
223
+ const lineNum = content.substring(0, match.index).split('\n').length;
224
+ const name = match[2];
225
+ metrics.classes++;
226
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
227
+ issues.push({
228
+ severity: 'warning', category: '命名',
229
+ message: `类名 '${name}' 不符合 PascalCase 规范`,
230
+ file_path: filePath, line_number: lineNum,
231
+ suggestion: '类名应使用 PascalCase,如 MyClassName',
232
+ });
233
+ }
234
+ }
235
+
236
+ metrics.functions = functions.length;
237
+ if (functions.length > 0) {
238
+ metrics.avg_function_length = functions.reduce((s, f) => s + f.length, 0) / functions.length;
239
+ }
240
+
241
+ return { metrics, issues };
242
+ }
243
+
244
+ // --- Directory scan ---
245
+
246
+ function scanDirectory(scanPath, excludeDirs) {
247
+ const resolved = path.resolve(scanPath);
248
+ const exclude = excludeDirs || EXCLUDE_DIRS;
249
+ const result = {
250
+ scan_path: resolved, files_scanned: 0,
251
+ total_lines: 0, total_code_lines: 0,
252
+ issues: [], file_metrics: [],
253
+ };
254
+
255
+ function walk(dir) {
256
+ let entries;
257
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
258
+ for (const entry of entries) {
259
+ if (exclude.has(entry.name)) continue;
260
+ const full = path.join(dir, entry.name);
261
+ if (entry.isDirectory()) { walk(full); continue; }
262
+ const ext = path.extname(entry.name).toLowerCase();
263
+ if (!CODE_EXTENSIONS.has(ext)) continue;
264
+
265
+ result.files_scanned++;
266
+ const { metrics, issues } = ext === '.py' ? analyzePythonFile(full) : analyzeGenericFile(full);
267
+ result.file_metrics.push(metrics);
268
+ result.issues.push(...issues);
269
+ result.total_lines += metrics.lines;
270
+ result.total_code_lines += metrics.code_lines;
271
+ }
272
+ }
273
+
274
+ walk(resolved);
275
+ return result;
276
+ }
277
+
278
+ // --- Reporting ---
279
+
280
+ function passed(result) { return !hasFatal(result.issues); }
281
+
282
+ function formatReport(result, verbose) {
283
+ const errs = result.issues.filter(i => i.severity === 'error').length;
284
+ const warns = result.issues.filter(i => i.severity === 'warning').length;
285
+ const fields = {
286
+ '扫描路径': result.scan_path,
287
+ '扫描文件': result.files_scanned,
288
+ '总行数': result.total_lines,
289
+ '代码行数': result.total_code_lines,
290
+ '检查结果': passed(result) ? '✓ 通过' : '✗ 需要关注',
291
+ '统计': `错误: ${errs} | 警告: ${warns}`,
292
+ };
293
+ let report = buildReport(
294
+ '代码质量检查报告', fields, result.issues, verbose, 'category'
295
+ );
296
+
297
+ if (verbose && result.file_metrics.length) {
298
+ const complex = result.file_metrics
299
+ .filter(m => m.max_complexity > 0)
300
+ .sort((a, b) => b.max_complexity - a.max_complexity)
301
+ .slice(0, 5);
302
+ if (complex.length) {
303
+ const lines = ['\n' + '-'.repeat(40), '复杂度最高的文件:', '-'.repeat(40)];
304
+ for (const m of complex) lines.push(` ${m.path}: 复杂度 ${m.max_complexity}, ${m.functions} 个函数`);
305
+ report += '\n' + lines.join('\n');
306
+ }
307
+ }
308
+ return report;
309
+ }
310
+
311
+ // --- CLI ---
312
+
313
+ function main() {
314
+ const opts = parseCliArgs(process.argv);
315
+
316
+ const result = scanDirectory(opts.target);
317
+
318
+ if (opts.json) {
319
+ const output = {
320
+ scan_path: result.scan_path,
321
+ files_scanned: result.files_scanned,
322
+ total_lines: result.total_lines,
323
+ total_code_lines: result.total_code_lines,
324
+ passed: passed(result),
325
+ error_count: result.issues.filter(i => i.severity === 'error').length,
326
+ warning_count: result.issues.filter(i => i.severity === 'warning').length,
327
+ issues: result.issues
328
+ };
329
+ console.log(JSON.stringify(output, null, 2));
330
+ } else {
331
+ console.log(formatReport(result, opts.verbose));
332
+ }
333
+
334
+ process.exit(passed(result) ? 0 : 1);
335
+ }
336
+
337
+ 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,283 @@
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
+ // prettier-ignore
10
+ const SECURITY_RULES = [
11
+ {
12
+ id: 'SQL_INJECTION_DYNAMIC', category: '注入',
13
+ severity: 'critical',
14
+ pattern: new RegExp(
15
+ '\\b(execute|query|raw)\\s*\\(\\s*' +
16
+ '(f["\']|["\'][^"\'\\n]*["\']\\s*\\+\\s*|["\'][^"\'\\n]*["\']\\s*%\\s*[^,)]|["\'][^"\'\\n]*["\']' +
17
+ '\\.format\\s*\\()', 'i'),
18
+ extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'],
19
+ message: '可能存在 SQL 注入风险',
20
+ recommendation: '使用参数化查询或 ORM',
21
+ },
22
+ {
23
+ id: 'SQL_INJECTION_FSTRING', category: '注入',
24
+ severity: 'critical',
25
+ pattern: /cursor\.(execute|executemany)\s*\(\s*f["']/i,
26
+ extensions: ['.py'],
27
+ message: '使用 f-string 构造 SQL 语句',
28
+ recommendation: '使用参数化查询',
29
+ },
30
+ {
31
+ id: 'COMMAND_INJECTION', category: '注入',
32
+ severity: 'critical',
33
+ pattern: /(os\.system|os\.popen|subprocess\.call|subprocess\.run|subprocess\.Popen)\s*\([^)]*shell\s*=\s*True/i,
34
+ extensions: ['.py'],
35
+ message: '使用 shell=True 可能导致命令注入',
36
+ recommendation: '避免 shell=True,使用列表参数',
37
+ },
38
+ {
39
+ id: 'COMMAND_INJECTION_EVAL', category: '注入',
40
+ severity: 'critical',
41
+ pattern: /\b(eval|exec)\s*\([^)]*\b(input|request|argv|args)/i,
42
+ extensions: ['.py'],
43
+ message: 'eval/exec 执行用户输入',
44
+ recommendation: '避免对用户输入使用 eval/exec',
45
+ },
46
+ {
47
+ id: 'HARDCODED_SECRET', category: '敏感信息',
48
+ severity: 'high',
49
+ pattern: /(?<!\w)(password|passwd|pwd|secret|api_key|apikey|token|auth_token)\s*=\s*["'][^"']{8,}["']/i,
50
+ excludePattern: /(example|placeholder|changeme|xxx|your[_-]|TODO|FIXME|<.*>|\*{3,})/i,
51
+ extensions: [
52
+ '.py', '.js', '.ts', '.go', '.java', '.php',
53
+ '.rb', '.yaml', '.yml', '.json', '.env',
54
+ ],
55
+ message: '可能存在硬编码密钥/密码',
56
+ recommendation: '使用环境变量或密钥管理服务',
57
+ },
58
+ {
59
+ id: 'HARDCODED_AWS_KEY', category: '敏感信息',
60
+ severity: 'critical',
61
+ pattern: /AKIA[0-9A-Z]{16}/,
62
+ extensions: ['*'],
63
+ message: '发现 AWS Access Key',
64
+ recommendation: '立即轮换密钥,使用 IAM 角色或环境变量',
65
+ },
66
+ {
67
+ id: 'HARDCODED_PRIVATE_KEY', category: '敏感信息',
68
+ severity: 'critical',
69
+ pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
70
+ extensions: ['*'],
71
+ message: '发现私钥',
72
+ recommendation: '私钥不应提交到代码库',
73
+ },
74
+ {
75
+ id: 'XSS_INNERHTML', category: 'XSS', severity: 'high',
76
+ pattern: /\.innerHTML\s*=|\.outerHTML\s*=|document\.write\s*\(/i,
77
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.html'],
78
+ message: '直接操作 innerHTML 可能导致 XSS',
79
+ recommendation: '使用 textContent 或框架的安全绑定',
80
+ },
81
+ {
82
+ id: 'XSS_DANGEROUSLY', category: 'XSS',
83
+ severity: 'medium',
84
+ pattern: /dangerouslySetInnerHTML/i,
85
+ extensions: ['.js', '.ts', '.jsx', '.tsx'],
86
+ message: '使用 dangerouslySetInnerHTML',
87
+ recommendation: '确保内容已经过净化处理',
88
+ },
89
+ {
90
+ id: 'UNSAFE_PICKLE', category: '反序列化',
91
+ severity: 'high',
92
+ pattern: /pickle\.loads?\s*\(|yaml\.load\s*\([^)]*Loader\s*=\s*yaml\.Loader/i,
93
+ extensions: ['.py'],
94
+ message: '不安全的反序列化',
95
+ recommendation: '使用 yaml.safe_load() 或验证数据来源',
96
+ },
97
+ {
98
+ id: 'WEAK_CRYPTO_MD5', category: '加密',
99
+ severity: 'medium',
100
+ pattern: /\b(md5|MD5)\s*\(|hashlib\.md5\s*\(/i,
101
+ extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'],
102
+ message: '使用弱哈希算法 MD5',
103
+ recommendation: '使用 bcrypt/argon2 或 SHA-256+',
104
+ },
105
+ {
106
+ id: 'WEAK_CRYPTO_SHA1', category: '加密',
107
+ severity: 'low',
108
+ pattern: /\b(sha1|SHA1)\s*\(|hashlib\.sha1\s*\(/i,
109
+ extensions: ['.py', '.js', '.ts', '.go', '.java', '.php'],
110
+ message: '使用弱哈希算法 SHA1',
111
+ recommendation: '使用 SHA-256 或更强的算法',
112
+ },
113
+ {
114
+ id: 'PATH_TRAVERSAL', category: '路径遍历',
115
+ severity: 'high',
116
+ pattern: new RegExp(
117
+ '(open|read|write|Path|os\\.path\\.join)\\s*\\([^\\n]*' +
118
+ '(request|input|argv|args|params|query|form|path_param)\\b', 'i'),
119
+ extensions: ['.py'],
120
+ message: '可能存在路径遍历风险',
121
+ recommendation: '验证并规范化用户输入的路径',
122
+ },
123
+ {
124
+ id: 'SSRF', category: 'SSRF', severity: 'high',
125
+ pattern: new RegExp(
126
+ '(requests\\.(get|post|put|delete|head)|urllib\\.request\\.urlopen)' +
127
+ '\\s*\\([^\\n]*(request|input|argv|args|params|query|url)\\b', 'i'),
128
+ extensions: ['.py'],
129
+ message: '可能存在 SSRF 风险',
130
+ recommendation: '验证并限制目标 URL',
131
+ },
132
+ {
133
+ id: 'DEBUG_CODE', category: '调试', severity: 'low',
134
+ pattern: /\b(console\.log|debugger|pdb\.set_trace|breakpoint)\s*\(/i,
135
+ extensions: ['.py', '.js', '.ts'],
136
+ message: '发现调试代码',
137
+ recommendation: '生产环境移除调试代码',
138
+ },
139
+ {
140
+ id: 'INSECURE_RANDOM', category: '加密',
141
+ severity: 'medium',
142
+ pattern: /\brandom\.(random|randint|choice|shuffle)\s*\(/i,
143
+ extensions: ['.py'],
144
+ message: '使用不安全的随机数生成器',
145
+ recommendation: '安全场景使用 secrets 模块',
146
+ },
147
+ {
148
+ id: 'XXE', category: 'XXE', severity: 'high',
149
+ pattern: /etree\.(parse|fromstring)\s*\([^)]*\)|xml\.dom\.minidom\.parse/i,
150
+ extensions: ['.py'],
151
+ message: 'XML 解析可能存在 XXE 风险',
152
+ recommendation: '禁用外部实体: XMLParser(resolve_entities=False)',
153
+ },
154
+ ];
155
+
156
+ const CODE_EXTENSIONS = new Set([
157
+ '.py', '.js', '.ts', '.jsx', '.tsx', '.go',
158
+ '.java', '.php', '.rb', '.yaml', '.yml', '.json',
159
+ ]);
160
+ const DEFAULT_EXCLUDES = [
161
+ '.git', 'node_modules', '__pycache__', '.venv', 'venv',
162
+ 'dist', 'build', '.tox', 'tests', 'test', '__tests__', 'spec',
163
+ ];
164
+
165
+ function scanFile(filePath, rules) {
166
+ const findings = [];
167
+ const ext = path.extname(filePath).toLowerCase();
168
+ let content;
169
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return findings; }
170
+ const lines = content.split('\n');
171
+
172
+ for (const rule of rules) {
173
+ const exts = rule.extensions;
174
+ if (!exts.includes('*') && !exts.includes(ext)) continue;
175
+
176
+ for (let i = 0; i < lines.length; i++) {
177
+ const line = lines[i];
178
+ const stripped = line.trim();
179
+ const isComment = stripped.startsWith('#') ||
180
+ stripped.startsWith('//') || stripped.startsWith('*') ||
181
+ stripped.startsWith('/*');
182
+ if (isComment) continue;
183
+ const ruleDefRe = /^\s*(id|pattern|severity|message|recommendation|extensions|excludePattern|category)\s*:/;
184
+ if (ruleDefRe.test(stripped)) continue;
185
+
186
+ if (rule.pattern.test(line)) {
187
+ rule.pattern.lastIndex = 0;
188
+ if (rule.excludePattern && rule.excludePattern.test(line)) {
189
+ rule.excludePattern.lastIndex = 0; continue;
190
+ }
191
+ findings.push({
192
+ severity: rule.severity, category: rule.category,
193
+ message: rule.message, file_path: filePath,
194
+ line_number: i + 1,
195
+ line_content: stripped.slice(0, 100),
196
+ recommendation: rule.recommendation,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ return findings;
202
+ }
203
+
204
+ function walkDir(dir, excludeDirs) {
205
+ const results = [];
206
+ let entries;
207
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
208
+ for (const entry of entries) {
209
+ if (excludeDirs.includes(entry.name)) continue;
210
+ const full = path.join(dir, entry.name);
211
+ if (entry.isDirectory()) { results.push(...walkDir(full, excludeDirs)); }
212
+ else if (entry.isFile()) {
213
+ if (CODE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
214
+ results.push(full);
215
+ }
216
+ }
217
+ }
218
+ return results;
219
+ }
220
+
221
+ function scanDirectory(scanPath, excludeDirs) {
222
+ const resolved = path.resolve(scanPath);
223
+ const findings = [];
224
+ const files = walkDir(resolved, excludeDirs);
225
+ for (const f of files) findings.push(...scanFile(f, SECURITY_RULES));
226
+ findings.sort((a, b) =>
227
+ (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
228
+ const passed = !findings.some(
229
+ f => f.severity === 'critical' || f.severity === 'high'
230
+ );
231
+ return { scan_path: resolved, files_scanned: files.length, passed, findings };
232
+ }
233
+
234
+ const { buildReport, countBySeverity, parseCliArgs } = require(
235
+ path.join(__dirname, '..', '..', 'lib', 'shared.js')
236
+ );
237
+
238
+ function formatReport(result, verbose) {
239
+ const counts = countBySeverity(result.findings);
240
+ const fields = {
241
+ '扫描路径': result.scan_path,
242
+ '扫描文件': result.files_scanned,
243
+ '扫描结果': result.passed ? '\u2713 通过' : '\u2717 发现高危问题',
244
+ '统计': `严重: ${counts.critical || 0} | 高危: ${counts.high || 0}` +
245
+ ` | 中危: ${counts.medium || 0} | 低危: ${counts.low || 0}`,
246
+ };
247
+ return buildReport(
248
+ '代码安全扫描报告', fields, result.findings, verbose, 'category'
249
+ );
250
+ }
251
+
252
+
253
+ function main() {
254
+ const opts = parseCliArgs(process.argv, { exclude: [] });
255
+ if (opts.help) {
256
+ console.log('Usage: security_scanner.js [path] [-v] [--json] [--exclude dir1 dir2]');
257
+ process.exit(0);
258
+ }
259
+ const scanPath = opts.target;
260
+ const verbose = opts.verbose;
261
+ const jsonOut = opts.json;
262
+ const excludeDirs = [...DEFAULT_EXCLUDES, ...opts.exclude];
263
+ const result = scanDirectory(scanPath, excludeDirs);
264
+
265
+ if (jsonOut) {
266
+ console.log(JSON.stringify({
267
+ scan_path: result.scan_path,
268
+ files_scanned: result.files_scanned,
269
+ passed: result.passed,
270
+ counts: countBySeverity(result.findings),
271
+ findings: result.findings,
272
+ }, null, 2));
273
+ } else {
274
+ console.log(formatReport(result, verbose));
275
+ }
276
+ process.exit(result.passed ? 0 : 1);
277
+ }
278
+
279
+ if (require.main === module) {
280
+ main();
281
+ }
282
+
283
+ module.exports = { scanFile, SECURITY_RULES };