@loxia-labs/loxia-autopilot-one 1.0.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.
- package/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecurityAnalyzer - Professional security scanning using external tools
|
|
3
|
+
*
|
|
4
|
+
* Uses industry-standard security scanners:
|
|
5
|
+
* - Semgrep: Multi-language SAST
|
|
6
|
+
* - Bandit: Python security scanner
|
|
7
|
+
* - ESLint Security Plugin: JavaScript/TypeScript security
|
|
8
|
+
* - npm audit: Node.js dependency vulnerabilities
|
|
9
|
+
* - pip-audit: Python dependency vulnerabilities
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { exec } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import { STATIC_ANALYSIS } from '../utilities/constants.js';
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
class SecurityAnalyzer {
|
|
21
|
+
constructor(logger = null) {
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.availableScanners = null;
|
|
24
|
+
this.scannerCache = new Map(); // Cache scanner availability checks
|
|
25
|
+
|
|
26
|
+
// Path to locally installed scanners (from postinstall script)
|
|
27
|
+
this.localScannerDir = path.join(process.cwd(), 'node_modules', '.scanners');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect which security scanners are available on the system
|
|
32
|
+
* @returns {Promise<Object>} Available scanners
|
|
33
|
+
*/
|
|
34
|
+
async detectAvailableScanners() {
|
|
35
|
+
// Return cached result if available
|
|
36
|
+
if (this.availableScanners !== null) {
|
|
37
|
+
return this.availableScanners;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const available = {
|
|
41
|
+
semgrep: false,
|
|
42
|
+
bandit: false,
|
|
43
|
+
npmAudit: false,
|
|
44
|
+
pipAudit: false,
|
|
45
|
+
eslintSecurity: false
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Check for Semgrep (local first, then system)
|
|
49
|
+
try {
|
|
50
|
+
const localSemgrep = path.join(this.localScannerDir, 'semgrep');
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(localSemgrep);
|
|
53
|
+
available.semgrep = true;
|
|
54
|
+
this.logger?.debug('Semgrep scanner detected (local)');
|
|
55
|
+
} catch {
|
|
56
|
+
await execAsync('semgrep --version', { timeout: 5000 });
|
|
57
|
+
available.semgrep = true;
|
|
58
|
+
this.logger?.debug('Semgrep scanner detected (system)');
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger?.debug('Semgrep not available', { error: error.message });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for Bandit
|
|
65
|
+
try {
|
|
66
|
+
await execAsync('bandit --version', { timeout: 5000 });
|
|
67
|
+
available.bandit = true;
|
|
68
|
+
this.logger?.debug('Bandit scanner detected');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.logger?.debug('Bandit not available', { error: error.message });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for npm (npm audit is built-in)
|
|
74
|
+
try {
|
|
75
|
+
await execAsync('npm --version', { timeout: 5000 });
|
|
76
|
+
available.npmAudit = true;
|
|
77
|
+
this.logger?.debug('npm audit available');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.logger?.debug('npm not available', { error: error.message });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for pip-audit
|
|
83
|
+
try {
|
|
84
|
+
await execAsync('pip-audit --version', { timeout: 5000 });
|
|
85
|
+
available.pipAudit = true;
|
|
86
|
+
this.logger?.debug('pip-audit detected');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger?.debug('pip-audit not available', { error: error.message });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for eslint-plugin-security
|
|
92
|
+
try {
|
|
93
|
+
// Check if the package is installed
|
|
94
|
+
const result = await execAsync('npm list eslint-plugin-security --depth=0 --json', {
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
cwd: process.cwd()
|
|
97
|
+
});
|
|
98
|
+
const parsed = JSON.parse(result.stdout);
|
|
99
|
+
if (parsed.dependencies && parsed.dependencies['eslint-plugin-security']) {
|
|
100
|
+
available.eslintSecurity = true;
|
|
101
|
+
this.logger?.debug('eslint-plugin-security detected');
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logger?.debug('eslint-plugin-security not available', { error: error.message });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.availableScanners = available;
|
|
108
|
+
return available;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Analyze a file for security vulnerabilities
|
|
113
|
+
* @param {string} filePath - Path to file
|
|
114
|
+
* @param {string} content - File content
|
|
115
|
+
* @param {Object} options - Analysis options
|
|
116
|
+
* @returns {Promise<Array>} Security issues found
|
|
117
|
+
*/
|
|
118
|
+
async analyze(filePath, content, options = {}) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
const available = await this.detectAvailableScanners();
|
|
121
|
+
const language = this.detectLanguage(filePath);
|
|
122
|
+
|
|
123
|
+
// Skip test files if requested
|
|
124
|
+
if (options.skipTestFiles !== false && this.isTestFile(filePath)) {
|
|
125
|
+
this.logger?.debug('Skipping test file for security scan', { filePath });
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Run appropriate scanners based on language
|
|
130
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
131
|
+
// Run Semgrep for JS/TS
|
|
132
|
+
if (available.semgrep) {
|
|
133
|
+
const semgrepIssues = await this.runSemgrep(filePath, [language], options);
|
|
134
|
+
issues.push(...semgrepIssues);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Run ESLint Security Plugin
|
|
138
|
+
if (available.eslintSecurity) {
|
|
139
|
+
const eslintIssues = await this.runESLintSecurity(filePath, content, options);
|
|
140
|
+
issues.push(...eslintIssues);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (language === 'python') {
|
|
145
|
+
// Run Bandit for Python
|
|
146
|
+
if (available.bandit) {
|
|
147
|
+
const banditIssues = await this.runBandit(filePath, options);
|
|
148
|
+
issues.push(...banditIssues);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Run Semgrep for Python
|
|
152
|
+
if (available.semgrep) {
|
|
153
|
+
const semgrepIssues = await this.runSemgrep(filePath, [language], options);
|
|
154
|
+
issues.push(...semgrepIssues);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If no scanners available, return informative message
|
|
159
|
+
if (issues.length === 0 && !this.hasScannersForLanguage(available, language)) {
|
|
160
|
+
this.logger?.warn('No security scanners available for language', { language, filePath });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return this.normalizeResults(issues);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Analyze a project directory for security vulnerabilities
|
|
168
|
+
* @param {string} projectDir - Project directory path
|
|
169
|
+
* @param {string} language - Primary language to scan
|
|
170
|
+
* @param {Object} options - Analysis options
|
|
171
|
+
* @returns {Promise<Array>} Security issues found
|
|
172
|
+
*/
|
|
173
|
+
async analyzeProject(projectDir, language, options = {}) {
|
|
174
|
+
const issues = [];
|
|
175
|
+
const available = await this.detectAvailableScanners();
|
|
176
|
+
|
|
177
|
+
// Run dependency scanners
|
|
178
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
179
|
+
if (available.npmAudit) {
|
|
180
|
+
const npmIssues = await this.runNpmAudit(projectDir, options);
|
|
181
|
+
issues.push(...npmIssues);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (language === 'python') {
|
|
186
|
+
if (available.pipAudit) {
|
|
187
|
+
const pipIssues = await this.runPipAudit(projectDir, options);
|
|
188
|
+
issues.push(...pipIssues);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.normalizeResults(issues);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run Semgrep scanner
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
async runSemgrep(filePath, languages, options = {}) {
|
|
200
|
+
try {
|
|
201
|
+
const dir = path.dirname(filePath);
|
|
202
|
+
const result = await execAsync(
|
|
203
|
+
`semgrep --config=auto --json "${filePath}"`,
|
|
204
|
+
{
|
|
205
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
206
|
+
timeout: 30000,
|
|
207
|
+
cwd: dir
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const output = JSON.parse(result.stdout);
|
|
212
|
+
return this.parseSemgrepResults(output);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// Semgrep exits with non-zero if issues found, check stdout
|
|
215
|
+
if (error.stdout) {
|
|
216
|
+
try {
|
|
217
|
+
const output = JSON.parse(error.stdout);
|
|
218
|
+
return this.parseSemgrepResults(output);
|
|
219
|
+
} catch (parseError) {
|
|
220
|
+
this.logger?.error('Failed to parse Semgrep output', {
|
|
221
|
+
error: parseError.message,
|
|
222
|
+
stdout: error.stdout
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.logger?.error('Semgrep scan failed', { error: error.message });
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Parse Semgrep results
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
parseSemgrepResults(output) {
|
|
236
|
+
const issues = [];
|
|
237
|
+
|
|
238
|
+
if (output.results && Array.isArray(output.results)) {
|
|
239
|
+
for (const result of output.results) {
|
|
240
|
+
issues.push({
|
|
241
|
+
file: result.path,
|
|
242
|
+
line: result.start?.line || 1,
|
|
243
|
+
column: result.start?.col || 1,
|
|
244
|
+
severity: this.mapSemgrepSeverity(result.extra?.severity),
|
|
245
|
+
rule: result.check_id,
|
|
246
|
+
message: result.extra?.message || result.extra?.lines || 'Security issue detected',
|
|
247
|
+
category: 'security',
|
|
248
|
+
scanner: 'semgrep',
|
|
249
|
+
cwe: result.extra?.metadata?.cwe,
|
|
250
|
+
owasp: result.extra?.metadata?.owasp,
|
|
251
|
+
confidence: result.extra?.metadata?.confidence,
|
|
252
|
+
references: result.extra?.metadata?.references
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return issues;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Run Bandit scanner for Python
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
async runBandit(filePath, options = {}) {
|
|
265
|
+
try {
|
|
266
|
+
const result = await execAsync(
|
|
267
|
+
`bandit -f json "${filePath}"`,
|
|
268
|
+
{
|
|
269
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
270
|
+
timeout: 30000
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const output = JSON.parse(result.stdout);
|
|
275
|
+
return this.parseBanditResults(output);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// Bandit exits with non-zero if issues found
|
|
278
|
+
if (error.stdout) {
|
|
279
|
+
try {
|
|
280
|
+
const output = JSON.parse(error.stdout);
|
|
281
|
+
return this.parseBanditResults(output);
|
|
282
|
+
} catch (parseError) {
|
|
283
|
+
this.logger?.error('Failed to parse Bandit output', {
|
|
284
|
+
error: parseError.message
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.logger?.error('Bandit scan failed', { error: error.message });
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Parse Bandit results
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
parseBanditResults(output) {
|
|
298
|
+
const issues = [];
|
|
299
|
+
|
|
300
|
+
if (output.results && Array.isArray(output.results)) {
|
|
301
|
+
for (const result of output.results) {
|
|
302
|
+
issues.push({
|
|
303
|
+
file: result.filename,
|
|
304
|
+
line: result.line_number || 1,
|
|
305
|
+
column: result.col_offset || 1,
|
|
306
|
+
severity: this.mapBanditSeverity(result.issue_severity),
|
|
307
|
+
rule: result.test_id,
|
|
308
|
+
message: result.issue_text,
|
|
309
|
+
category: 'security',
|
|
310
|
+
scanner: 'bandit',
|
|
311
|
+
cwe: result.issue_cwe?.id ? `CWE-${result.issue_cwe.id}` : null,
|
|
312
|
+
confidence: result.issue_confidence,
|
|
313
|
+
moreInfo: result.more_info
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return issues;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Run ESLint with security plugin
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
async runESLintSecurity(filePath, content, options = {}) {
|
|
326
|
+
try {
|
|
327
|
+
// Use ESLint programmatically
|
|
328
|
+
const { ESLint } = await import('eslint');
|
|
329
|
+
|
|
330
|
+
const eslint = new ESLint({
|
|
331
|
+
overrideConfig: {
|
|
332
|
+
plugins: ['security'],
|
|
333
|
+
extends: ['plugin:security/recommended'],
|
|
334
|
+
parserOptions: {
|
|
335
|
+
ecmaVersion: 'latest',
|
|
336
|
+
sourceType: 'module'
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
useEslintrc: false
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const results = await eslint.lintText(content, { filePath });
|
|
343
|
+
return this.parseESLintResults(results, filePath);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.logger?.error('ESLint security scan failed', {
|
|
346
|
+
error: error.message,
|
|
347
|
+
filePath
|
|
348
|
+
});
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse ESLint security results
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
parseESLintResults(results, filePath) {
|
|
358
|
+
const issues = [];
|
|
359
|
+
|
|
360
|
+
for (const result of results) {
|
|
361
|
+
if (result.messages && Array.isArray(result.messages)) {
|
|
362
|
+
for (const message of result.messages) {
|
|
363
|
+
// Only include security plugin rules
|
|
364
|
+
if (message.ruleId && message.ruleId.startsWith('security/')) {
|
|
365
|
+
issues.push({
|
|
366
|
+
file: filePath,
|
|
367
|
+
line: message.line || 1,
|
|
368
|
+
column: message.column || 1,
|
|
369
|
+
severity: this.mapESLintSeverity(message.severity),
|
|
370
|
+
rule: message.ruleId,
|
|
371
|
+
message: message.message,
|
|
372
|
+
category: 'security',
|
|
373
|
+
scanner: 'eslint-security',
|
|
374
|
+
fixable: message.fix !== undefined
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return issues;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Run npm audit for dependency vulnerabilities
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
async runNpmAudit(projectDir, options = {}) {
|
|
389
|
+
try {
|
|
390
|
+
// Check if package.json exists
|
|
391
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
392
|
+
try {
|
|
393
|
+
await fs.access(packageJsonPath);
|
|
394
|
+
} catch {
|
|
395
|
+
this.logger?.debug('No package.json found, skipping npm audit');
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = await execAsync(
|
|
400
|
+
'npm audit --json',
|
|
401
|
+
{
|
|
402
|
+
cwd: projectDir,
|
|
403
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
404
|
+
timeout: 60000
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const output = JSON.parse(result.stdout);
|
|
409
|
+
return this.parseNpmAuditResults(output);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
// npm audit exits with non-zero if vulnerabilities found
|
|
412
|
+
if (error.stdout) {
|
|
413
|
+
try {
|
|
414
|
+
const output = JSON.parse(error.stdout);
|
|
415
|
+
return this.parseNpmAuditResults(output);
|
|
416
|
+
} catch (parseError) {
|
|
417
|
+
this.logger?.error('Failed to parse npm audit output', {
|
|
418
|
+
error: parseError.message
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Parse npm audit results
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
parseNpmAuditResults(output) {
|
|
431
|
+
const issues = [];
|
|
432
|
+
|
|
433
|
+
// npm audit v7+ format
|
|
434
|
+
if (output.vulnerabilities) {
|
|
435
|
+
for (const [packageName, vuln] of Object.entries(output.vulnerabilities)) {
|
|
436
|
+
issues.push({
|
|
437
|
+
file: 'package.json',
|
|
438
|
+
line: 1,
|
|
439
|
+
column: 1,
|
|
440
|
+
severity: this.mapNpmSeverity(vuln.severity),
|
|
441
|
+
rule: `npm-${vuln.via[0]?.source || 'advisory'}`,
|
|
442
|
+
message: `${packageName}: ${vuln.via[0]?.title || 'Security vulnerability'}`,
|
|
443
|
+
category: 'security',
|
|
444
|
+
scanner: 'npm-audit',
|
|
445
|
+
package: packageName,
|
|
446
|
+
vulnerableVersions: vuln.range,
|
|
447
|
+
patchedVersions: vuln.fixAvailable ? 'Available' : 'None',
|
|
448
|
+
cve: vuln.via[0]?.cve,
|
|
449
|
+
cvss: vuln.via[0]?.cvss,
|
|
450
|
+
references: vuln.via[0]?.url ? [vuln.via[0].url] : []
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return issues;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Run pip-audit for Python dependencies
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
async runPipAudit(projectDir, options = {}) {
|
|
463
|
+
try {
|
|
464
|
+
// Check if requirements.txt exists
|
|
465
|
+
const requirementsPath = path.join(projectDir, 'requirements.txt');
|
|
466
|
+
try {
|
|
467
|
+
await fs.access(requirementsPath);
|
|
468
|
+
} catch {
|
|
469
|
+
this.logger?.debug('No requirements.txt found, skipping pip-audit');
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const result = await execAsync(
|
|
474
|
+
'pip-audit --format json',
|
|
475
|
+
{
|
|
476
|
+
cwd: projectDir,
|
|
477
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
478
|
+
timeout: 60000
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const output = JSON.parse(result.stdout);
|
|
483
|
+
return this.parsePipAuditResults(output);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error.stdout) {
|
|
486
|
+
try {
|
|
487
|
+
const output = JSON.parse(error.stdout);
|
|
488
|
+
return this.parsePipAuditResults(output);
|
|
489
|
+
} catch (parseError) {
|
|
490
|
+
this.logger?.error('Failed to parse pip-audit output', {
|
|
491
|
+
error: parseError.message
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Parse pip-audit results
|
|
501
|
+
* @private
|
|
502
|
+
*/
|
|
503
|
+
parsePipAuditResults(output) {
|
|
504
|
+
const issues = [];
|
|
505
|
+
|
|
506
|
+
if (output.dependencies && Array.isArray(output.dependencies)) {
|
|
507
|
+
for (const dep of output.dependencies) {
|
|
508
|
+
if (dep.vulns && Array.isArray(dep.vulns)) {
|
|
509
|
+
for (const vuln of dep.vulns) {
|
|
510
|
+
issues.push({
|
|
511
|
+
file: 'requirements.txt',
|
|
512
|
+
line: 1,
|
|
513
|
+
column: 1,
|
|
514
|
+
severity: this.mapPipAuditSeverity(vuln.severity),
|
|
515
|
+
rule: vuln.id,
|
|
516
|
+
message: `${dep.name}: ${vuln.description || 'Security vulnerability'}`,
|
|
517
|
+
category: 'security',
|
|
518
|
+
scanner: 'pip-audit',
|
|
519
|
+
package: dep.name,
|
|
520
|
+
vulnerableVersion: dep.version,
|
|
521
|
+
fixedVersions: vuln.fix_versions,
|
|
522
|
+
references: vuln.aliases || []
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return issues;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Detect language from file extension
|
|
534
|
+
* @private
|
|
535
|
+
*/
|
|
536
|
+
detectLanguage(filePath) {
|
|
537
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
538
|
+
const languageMap = {
|
|
539
|
+
'.js': 'javascript',
|
|
540
|
+
'.jsx': 'javascript',
|
|
541
|
+
'.mjs': 'javascript',
|
|
542
|
+
'.cjs': 'javascript',
|
|
543
|
+
'.ts': 'typescript',
|
|
544
|
+
'.tsx': 'typescript',
|
|
545
|
+
'.py': 'python'
|
|
546
|
+
};
|
|
547
|
+
return languageMap[ext] || null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check if file is a test file
|
|
552
|
+
* @private
|
|
553
|
+
*/
|
|
554
|
+
isTestFile(filePath) {
|
|
555
|
+
const testPatterns = [
|
|
556
|
+
/\.test\./,
|
|
557
|
+
/\.spec\./,
|
|
558
|
+
/__tests__\//,
|
|
559
|
+
/\/tests?\//,
|
|
560
|
+
/\.test$/,
|
|
561
|
+
/\.spec$/
|
|
562
|
+
];
|
|
563
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Check if scanners are available for a language
|
|
568
|
+
* @private
|
|
569
|
+
*/
|
|
570
|
+
hasScannersForLanguage(available, language) {
|
|
571
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
572
|
+
return available.semgrep || available.eslintSecurity;
|
|
573
|
+
}
|
|
574
|
+
if (language === 'python') {
|
|
575
|
+
return available.bandit || available.semgrep;
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Normalize all scanner results to common format
|
|
582
|
+
* @private
|
|
583
|
+
*/
|
|
584
|
+
normalizeResults(results) {
|
|
585
|
+
return results.map(result => ({
|
|
586
|
+
file: result.file,
|
|
587
|
+
line: result.line || 1,
|
|
588
|
+
column: result.column || 1,
|
|
589
|
+
severity: result.severity || STATIC_ANALYSIS.SEVERITY.WARNING,
|
|
590
|
+
rule: result.rule || 'unknown',
|
|
591
|
+
message: result.message || 'Security issue detected',
|
|
592
|
+
category: 'security',
|
|
593
|
+
scanner: result.scanner,
|
|
594
|
+
cwe: result.cwe || null,
|
|
595
|
+
owasp: result.owasp || null,
|
|
596
|
+
confidence: result.confidence || null,
|
|
597
|
+
fixable: result.fixable || false,
|
|
598
|
+
remediation: result.remediation || result.moreInfo || null,
|
|
599
|
+
references: result.references || [],
|
|
600
|
+
package: result.package || null
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Map Semgrep severity to our standard
|
|
606
|
+
* @private
|
|
607
|
+
*/
|
|
608
|
+
mapSemgrepSeverity(severity) {
|
|
609
|
+
const map = {
|
|
610
|
+
'ERROR': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
611
|
+
'WARNING': STATIC_ANALYSIS.SEVERITY.ERROR,
|
|
612
|
+
'INFO': STATIC_ANALYSIS.SEVERITY.WARNING
|
|
613
|
+
};
|
|
614
|
+
return map[severity?.toUpperCase()] || STATIC_ANALYSIS.SEVERITY.WARNING;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Map Bandit severity to our standard
|
|
619
|
+
* @private
|
|
620
|
+
*/
|
|
621
|
+
mapBanditSeverity(severity) {
|
|
622
|
+
const map = {
|
|
623
|
+
'HIGH': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
624
|
+
'MEDIUM': STATIC_ANALYSIS.SEVERITY.ERROR,
|
|
625
|
+
'LOW': STATIC_ANALYSIS.SEVERITY.WARNING
|
|
626
|
+
};
|
|
627
|
+
return map[severity?.toUpperCase()] || STATIC_ANALYSIS.SEVERITY.WARNING;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Map ESLint severity to our standard
|
|
632
|
+
* @private
|
|
633
|
+
*/
|
|
634
|
+
mapESLintSeverity(severity) {
|
|
635
|
+
return severity === 2 ? STATIC_ANALYSIS.SEVERITY.ERROR : STATIC_ANALYSIS.SEVERITY.WARNING;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Map npm audit severity to our standard
|
|
640
|
+
* @private
|
|
641
|
+
*/
|
|
642
|
+
mapNpmSeverity(severity) {
|
|
643
|
+
const map = {
|
|
644
|
+
'critical': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
645
|
+
'high': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
646
|
+
'moderate': STATIC_ANALYSIS.SEVERITY.ERROR,
|
|
647
|
+
'low': STATIC_ANALYSIS.SEVERITY.WARNING,
|
|
648
|
+
'info': STATIC_ANALYSIS.SEVERITY.INFO
|
|
649
|
+
};
|
|
650
|
+
return map[severity?.toLowerCase()] || STATIC_ANALYSIS.SEVERITY.WARNING;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Map pip-audit severity to our standard
|
|
655
|
+
* @private
|
|
656
|
+
*/
|
|
657
|
+
mapPipAuditSeverity(severity) {
|
|
658
|
+
// pip-audit doesn't always provide severity, default to ERROR
|
|
659
|
+
if (!severity) return STATIC_ANALYSIS.SEVERITY.ERROR;
|
|
660
|
+
|
|
661
|
+
const map = {
|
|
662
|
+
'critical': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
663
|
+
'high': STATIC_ANALYSIS.SEVERITY.CRITICAL,
|
|
664
|
+
'medium': STATIC_ANALYSIS.SEVERITY.ERROR,
|
|
665
|
+
'low': STATIC_ANALYSIS.SEVERITY.WARNING
|
|
666
|
+
};
|
|
667
|
+
return map[severity?.toLowerCase()] || STATIC_ANALYSIS.SEVERITY.ERROR;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get scanner status report
|
|
672
|
+
* @returns {Promise<Object>} Scanner availability and status
|
|
673
|
+
*/
|
|
674
|
+
async getScannerStatus() {
|
|
675
|
+
const available = await this.detectAvailableScanners();
|
|
676
|
+
return {
|
|
677
|
+
scanners: available,
|
|
678
|
+
recommendations: this.getInstallRecommendations(available)
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get installation recommendations for missing scanners
|
|
684
|
+
* @private
|
|
685
|
+
*/
|
|
686
|
+
getInstallRecommendations(available) {
|
|
687
|
+
const recommendations = [];
|
|
688
|
+
|
|
689
|
+
if (!available.semgrep) {
|
|
690
|
+
recommendations.push({
|
|
691
|
+
scanner: 'Semgrep',
|
|
692
|
+
reason: 'Multi-language SAST with extensive security rules',
|
|
693
|
+
install: 'pip install semgrep OR use Docker: docker pull returntocorp/semgrep',
|
|
694
|
+
priority: 'high'
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!available.bandit) {
|
|
699
|
+
recommendations.push({
|
|
700
|
+
scanner: 'Bandit',
|
|
701
|
+
reason: 'Python security scanner',
|
|
702
|
+
install: 'pip install bandit',
|
|
703
|
+
priority: 'medium'
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (!available.eslintSecurity) {
|
|
708
|
+
recommendations.push({
|
|
709
|
+
scanner: 'eslint-plugin-security',
|
|
710
|
+
reason: 'JavaScript/TypeScript security rules',
|
|
711
|
+
install: 'npm install --save-dev eslint-plugin-security',
|
|
712
|
+
priority: 'medium'
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!available.pipAudit) {
|
|
717
|
+
recommendations.push({
|
|
718
|
+
scanner: 'pip-audit',
|
|
719
|
+
reason: 'Python dependency vulnerability scanner',
|
|
720
|
+
install: 'pip install pip-audit',
|
|
721
|
+
priority: 'low'
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return recommendations;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export default SecurityAnalyzer;
|