code-abyss 1.7.0 → 1.7.2

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.
@@ -6,28 +6,161 @@ const path = require('path');
6
6
 
7
7
  const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
8
8
 
9
+ // prettier-ignore
9
10
  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)' },
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
+ },
27
154
  ];
28
155
 
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'];
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
+ ];
31
164
 
32
165
  function scanFile(filePath, rules) {
33
166
  const findings = [];
@@ -43,12 +176,25 @@ function scanFile(filePath, rules) {
43
176
  for (let i = 0; i < lines.length; i++) {
44
177
  const line = lines[i];
45
178
  const stripped = line.trim();
46
- if (stripped.startsWith('#') || stripped.startsWith('//') || stripped.startsWith('*') || stripped.startsWith('/*')) continue;
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;
47
185
 
48
186
  if (rule.pattern.test(line)) {
49
187
  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 });
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
+ });
52
198
  }
53
199
  }
54
200
  }
@@ -63,7 +209,11 @@ function walkDir(dir, excludeDirs) {
63
209
  if (excludeDirs.includes(entry.name)) continue;
64
210
  const full = path.join(dir, entry.name);
65
211
  if (entry.isDirectory()) { results.push(...walkDir(full, excludeDirs)); }
66
- else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) { results.push(full); }
212
+ else if (entry.isFile()) {
213
+ if (CODE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
214
+ results.push(full);
215
+ }
216
+ }
67
217
  }
68
218
  return results;
69
219
  }
@@ -73,61 +223,61 @@ function scanDirectory(scanPath, excludeDirs) {
73
223
  const findings = [];
74
224
  const files = walkDir(resolved, excludeDirs);
75
225
  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');
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
+ );
78
231
  return { scan_path: resolved, files_scanned: files.length, passed, findings };
79
232
  }
80
233
 
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
- }
234
+ const { buildReport, countBySeverity, parseCliArgs } = require(
235
+ path.join(__dirname, '..', '..', 'lib', 'shared.js')
236
+ );
86
237
 
87
238
  function formatReport(result, verbose) {
88
239
  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');
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
+ );
108
250
  }
109
251
 
252
+
110
253
  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];
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);
120
258
  }
121
-
122
- const excludeDirs = [...DEFAULT_EXCLUDES, ...extraExcludes];
259
+ const scanPath = opts.target;
260
+ const verbose = opts.verbose;
261
+ const jsonOut = opts.json;
262
+ const excludeDirs = [...DEFAULT_EXCLUDES, ...opts.exclude];
123
263
  const result = scanDirectory(scanPath, excludeDirs);
124
264
 
125
265
  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));
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));
127
273
  } else {
128
274
  console.log(formatReport(result, verbose));
129
275
  }
130
276
  process.exit(result.passed ? 0 : 1);
131
277
  }
132
278
 
133
- main();
279
+ if (require.main === module) {
280
+ main();
281
+ }
282
+
283
+ module.exports = { scanFile, SECURITY_RULES };