nayan-ai 1.0.0-beta.3 → 1.0.0-beta.5
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/README.md +101 -14
- package/dist/cli.js +21 -157
- package/dist/cli.js.map +1 -1
- package/dist/common/claude.d.ts +6 -0
- package/dist/common/claude.js +180 -0
- package/dist/common/claude.js.map +1 -0
- package/dist/common/codex.d.ts +6 -0
- package/dist/common/codex.js +169 -0
- package/dist/common/codex.js.map +1 -0
- package/dist/common/github.d.ts +22 -0
- package/dist/common/github.js +153 -0
- package/dist/common/github.js.map +1 -0
- package/dist/common/logs.d.ts +15 -0
- package/dist/common/logs.js +154 -0
- package/dist/common/logs.js.map +1 -0
- package/dist/{types.d.ts → common/types.d.ts} +36 -1
- package/dist/common/types.js.map +1 -0
- package/dist/common/utils.d.ts +3 -0
- package/dist/common/utils.js +11 -0
- package/dist/common/utils.js.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/{analyzer.d.ts → review/analyzer.d.ts} +1 -1
- package/dist/review/analyzer.js.map +1 -0
- package/dist/review/command.d.ts +2 -0
- package/dist/review/command.js +111 -0
- package/dist/review/command.js.map +1 -0
- package/dist/review/prompt.js.map +1 -0
- package/dist/scan/command.d.ts +2 -0
- package/dist/scan/command.js +700 -0
- package/dist/scan/command.js.map +1 -0
- package/dist/scan/fixer.d.ts +30 -0
- package/dist/scan/fixer.js +264 -0
- package/dist/scan/fixer.js.map +1 -0
- package/dist/scan/prompt.d.ts +4 -0
- package/dist/scan/prompt.js +175 -0
- package/dist/scan/prompt.js.map +1 -0
- package/package.json +2 -2
- package/dist/analyzer.js.map +0 -1
- package/dist/claude.d.ts +0 -5
- package/dist/claude.js +0 -128
- package/dist/claude.js.map +0 -1
- package/dist/codex.d.ts +0 -5
- package/dist/codex.js +0 -129
- package/dist/codex.js.map +0 -1
- package/dist/github.d.ts +0 -15
- package/dist/github.js +0 -88
- package/dist/github.js.map +0 -1
- package/dist/logs.d.ts +0 -34
- package/dist/logs.js +0 -219
- package/dist/logs.js.map +0 -1
- package/dist/prompt.js.map +0 -1
- package/dist/repo.d.ts +0 -8
- package/dist/repo.js +0 -61
- package/dist/repo.js.map +0 -1
- package/dist/types.js.map +0 -1
- /package/dist/{types.js → common/types.js} +0 -0
- /package/dist/{analyzer.js → review/analyzer.js} +0 -0
- /package/dist/{prompt.d.ts → review/prompt.d.ts} +0 -0
- /package/dist/{prompt.js → review/prompt.js} +0 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { cloneRepoForScan, parseRepoReference } from '../common/github.js';
|
|
7
|
+
import { analyzeWithCodex } from '../common/codex.js';
|
|
8
|
+
import { analyzeWithClaude } from '../common/claude.js';
|
|
9
|
+
import { getScanPrompt } from './prompt.js';
|
|
10
|
+
import { VALID_LLM_PROVIDERS, checkLLMAvailability } from '../common/utils.js';
|
|
11
|
+
import { fixVulnerabilities, runFixWorkflow } from './fixer.js';
|
|
12
|
+
const PROJECT_MARKERS = {
|
|
13
|
+
npm: { manifest: 'package.json', lockFiles: ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb'] },
|
|
14
|
+
python: { manifest: 'requirements.txt', lockFiles: ['Pipfile.lock', 'poetry.lock'] },
|
|
15
|
+
go: { manifest: 'go.mod', lockFiles: ['go.sum'] },
|
|
16
|
+
rust: { manifest: 'Cargo.toml', lockFiles: ['Cargo.lock'] },
|
|
17
|
+
ruby: { manifest: 'Gemfile', lockFiles: ['Gemfile.lock'] },
|
|
18
|
+
php: { manifest: 'composer.json', lockFiles: ['composer.lock'] },
|
|
19
|
+
java: { manifest: 'pom.xml', lockFiles: [] },
|
|
20
|
+
dotnet: { manifest: '*.csproj', lockFiles: ['packages.lock.json'] },
|
|
21
|
+
};
|
|
22
|
+
export const scanCommand = async (repoUrl, options) => {
|
|
23
|
+
try {
|
|
24
|
+
if (!VALID_LLM_PROVIDERS.includes(options.llm)) {
|
|
25
|
+
console.error(chalk.red(`Error: Invalid LLM provider '${options.llm}'. Valid options: ${VALID_LLM_PROVIDERS.join(', ')}`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
checkLLMAvailability(options.llm);
|
|
29
|
+
const repoInfo = parseRepoReference(repoUrl);
|
|
30
|
+
console.log(chalk.bold.blue('\n🤖 Nayan AI - Vulnerability Scanner'));
|
|
31
|
+
console.log('━'.repeat(40));
|
|
32
|
+
console.log(` Repository: ${chalk.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
|
|
33
|
+
console.log(` LLM: ${chalk.cyan(options.llm === 'claude' ? 'Claude Code' : 'Codex')}`);
|
|
34
|
+
if (repoInfo.githubUrl)
|
|
35
|
+
console.log(` GitHub: ${chalk.cyan(repoInfo.githubUrl)}`);
|
|
36
|
+
if (options.paths)
|
|
37
|
+
console.log(` Paths: ${chalk.cyan(options.paths)}`);
|
|
38
|
+
if (options.fix)
|
|
39
|
+
console.log(` Mode: ${chalk.green('Auto-fix enabled (will create PR)')}`);
|
|
40
|
+
console.log('━'.repeat(40) + '\n');
|
|
41
|
+
// Clone repository
|
|
42
|
+
let spinner = ora('Cloning repository...').start();
|
|
43
|
+
const repo = await cloneRepoForScan(repoInfo, options.token);
|
|
44
|
+
spinner.succeed('Repository cloned');
|
|
45
|
+
const targetPath = repo.path;
|
|
46
|
+
let projects;
|
|
47
|
+
try {
|
|
48
|
+
if (options.paths) {
|
|
49
|
+
// User specified paths to scan for projects
|
|
50
|
+
const scanPaths = options.paths.split(',').map(p => p.trim());
|
|
51
|
+
projects = [];
|
|
52
|
+
for (const scanPath of scanPaths) {
|
|
53
|
+
const fullPath = path.join(targetPath, scanPath);
|
|
54
|
+
spinner = ora(`Detecting projects in ${scanPath}...`).start();
|
|
55
|
+
const detected = detectProjects(fullPath);
|
|
56
|
+
if (detected.length > 0) {
|
|
57
|
+
projects.push(...detected);
|
|
58
|
+
spinner.succeed(`Found ${detected.length} project(s) in ${scanPath}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
spinner.warn(`No supported projects found in ${scanPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Auto-detect all projects in repo root
|
|
67
|
+
spinner = ora('Detecting projects...').start();
|
|
68
|
+
projects = detectProjects(targetPath);
|
|
69
|
+
spinner.succeed(`Found ${projects.length} project(s)`);
|
|
70
|
+
}
|
|
71
|
+
if (projects.length === 0) {
|
|
72
|
+
console.log(chalk.yellow('\nNo supported projects found.'));
|
|
73
|
+
console.log(chalk.gray('Supported: npm, Python, Go, Rust, Ruby, PHP, Java (Maven), .NET'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
const relativePath = path.relative(targetPath, project.path) || '.';
|
|
79
|
+
spinner = ora(`Scanning ${chalk.cyan(project.type)} project: ${relativePath}`).start();
|
|
80
|
+
try {
|
|
81
|
+
// Native scanner is the primary source of truth for consistent CVE detection
|
|
82
|
+
const nativeVulns = await scanProjectNative(project);
|
|
83
|
+
spinner.succeed(`Native scan complete for ${project.type} (${relativePath}) - ${nativeVulns.length} vulnerabilities`);
|
|
84
|
+
// Use native results as the base - AI adds supplementary analysis
|
|
85
|
+
let allVulns = [...nativeVulns];
|
|
86
|
+
// Always run AI analysis:
|
|
87
|
+
// - If native found vulns: AI verifies and adds context
|
|
88
|
+
// - If native found nothing: AI may find vulns that native missed (e.g., no lock file)
|
|
89
|
+
const llmName = options.llm === 'claude' ? 'Claude Code' : 'Codex';
|
|
90
|
+
console.log(chalk.cyan(` Running AI analysis with ${llmName}...\n`));
|
|
91
|
+
try {
|
|
92
|
+
const aiVulns = await scanProjectWithAI(project, options, nativeVulns);
|
|
93
|
+
if (nativeVulns.length > 0) {
|
|
94
|
+
// Native found vulns - only add NEW AI findings
|
|
95
|
+
const newAiVulns = aiVulns.filter(av => !nativeVulns.some(nv => nv.package === av.package && nv.version === av.version));
|
|
96
|
+
if (newAiVulns.length > 0) {
|
|
97
|
+
console.log(chalk.gray(` AI found ${newAiVulns.length} additional potential issues`));
|
|
98
|
+
allVulns = [...nativeVulns, ...newAiVulns];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Native found nothing - use AI results as primary
|
|
103
|
+
allVulns = aiVulns;
|
|
104
|
+
if (aiVulns.length > 0) {
|
|
105
|
+
console.log(chalk.gray(` AI detected ${aiVulns.length} vulnerabilities`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (aiError) {
|
|
110
|
+
// AI failure should not affect native results
|
|
111
|
+
console.log(chalk.gray(` AI analysis skipped: ${aiError instanceof Error ? aiError.message : String(aiError)}`));
|
|
112
|
+
}
|
|
113
|
+
results.push({
|
|
114
|
+
projectPath: project.path,
|
|
115
|
+
projectType: project.type,
|
|
116
|
+
vulnerabilities: allVulns,
|
|
117
|
+
});
|
|
118
|
+
if (allVulns.length === 0) {
|
|
119
|
+
console.log(chalk.green(` ✔ ${project.type} (${relativePath}): No vulnerabilities found`));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(chalk.yellow(` ⚠ ${project.type} (${relativePath}): ${allVulns.length} vulnerabilities found`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.log(chalk.red(` ✖ ${project.type} (${relativePath}): Scan failed`));
|
|
127
|
+
console.log(chalk.gray(` ${error instanceof Error ? error.message : String(error)}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Display results
|
|
131
|
+
console.log(chalk.bold('\n📋 Scan Summary'));
|
|
132
|
+
console.log('─'.repeat(41));
|
|
133
|
+
if (options.format === 'json') {
|
|
134
|
+
console.log(JSON.stringify(results, null, 2));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
printScanResults(results, targetPath);
|
|
138
|
+
}
|
|
139
|
+
const totalVulns = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
|
|
140
|
+
if (totalVulns > 0) {
|
|
141
|
+
console.log(chalk.yellow(`\n⚠ Found ${totalVulns} total vulnerabilities across ${results.length} project(s)`));
|
|
142
|
+
// Generate fixes and show in summary
|
|
143
|
+
console.log(chalk.bold.blue('\n🔧 Suggested Fixes'));
|
|
144
|
+
console.log('─'.repeat(41));
|
|
145
|
+
const fixResults = [];
|
|
146
|
+
for (const result of results) {
|
|
147
|
+
if (result.vulnerabilities.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
const project = projects.find(p => p.path === result.projectPath);
|
|
150
|
+
if (!project)
|
|
151
|
+
continue;
|
|
152
|
+
const relativePath = path.relative(targetPath, result.projectPath) || '.';
|
|
153
|
+
console.log(chalk.cyan(`\n Generating fixes for ${project.type} (${relativePath})...`));
|
|
154
|
+
const fixResult = await fixVulnerabilities(project, result.vulnerabilities, options);
|
|
155
|
+
if (fixResult) {
|
|
156
|
+
fixResults.push(fixResult);
|
|
157
|
+
console.log(chalk.green(` ✔ Generated ${fixResult.fixes.length} fixes`));
|
|
158
|
+
// Show fix details in summary
|
|
159
|
+
printFixSummary(fixResult);
|
|
160
|
+
if (fixResult.breakingChanges.length > 0) {
|
|
161
|
+
console.log(chalk.yellow(`\n ⚠ Potential breaking changes:`));
|
|
162
|
+
fixResult.breakingChanges.forEach(c => console.log(chalk.yellow(` - ${c}`)));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Only create PR if --fix flag is used
|
|
167
|
+
if (options.fix && fixResults.length > 0) {
|
|
168
|
+
console.log(chalk.bold('\n📤 Creating Pull Request'));
|
|
169
|
+
console.log('─'.repeat(41));
|
|
170
|
+
await runFixWorkflow(targetPath, repoInfo, fixResults, options);
|
|
171
|
+
}
|
|
172
|
+
else if (fixResults.length > 0) {
|
|
173
|
+
console.log(chalk.gray('\n 💡 Use --fix flag to automatically create a PR with these fixes'));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(chalk.yellow('\n No fixes could be generated'));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log(chalk.green('\n✅ No vulnerabilities found!\n'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await repo.cleanup();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error(chalk.red('\nError:'), error instanceof Error ? error.message : String(error));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function detectProjects(rootPath, maxDepth = 5) {
|
|
193
|
+
const projects = [];
|
|
194
|
+
function scan(dir, depth) {
|
|
195
|
+
if (depth > maxDepth)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
// Check for project markers in current directory
|
|
200
|
+
for (const [type, markers] of Object.entries(PROJECT_MARKERS)) {
|
|
201
|
+
const manifestPattern = markers.manifest;
|
|
202
|
+
let hasManifest = false;
|
|
203
|
+
if (manifestPattern.includes('*')) {
|
|
204
|
+
// Glob pattern (e.g., *.csproj)
|
|
205
|
+
const ext = manifestPattern.replace('*', '');
|
|
206
|
+
hasManifest = entries.some(e => e.isFile() && e.name.endsWith(ext));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
hasManifest = entries.some(e => e.isFile() && e.name === manifestPattern);
|
|
210
|
+
}
|
|
211
|
+
if (hasManifest) {
|
|
212
|
+
const lockFile = markers.lockFiles.find(lf => entries.some(e => e.isFile() && e.name === lf));
|
|
213
|
+
projects.push({
|
|
214
|
+
path: dir,
|
|
215
|
+
type,
|
|
216
|
+
lockFile: lockFile ? path.join(dir, lockFile) : undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Recurse into subdirectories (skip node_modules, vendor, etc.)
|
|
221
|
+
const skipDirs = ['node_modules', 'vendor', '.git', 'dist', 'build', '__pycache__', 'venv', '.venv', 'target'];
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.isDirectory() && !skipDirs.includes(entry.name)) {
|
|
224
|
+
scan(path.join(dir, entry.name), depth + 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Permission denied or other error, skip
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
scan(rootPath, 0);
|
|
233
|
+
return projects;
|
|
234
|
+
}
|
|
235
|
+
function detectProjectType(projectPath) {
|
|
236
|
+
if (!fs.existsSync(projectPath))
|
|
237
|
+
return null;
|
|
238
|
+
const stat = fs.statSync(projectPath);
|
|
239
|
+
const dir = stat.isDirectory() ? projectPath : path.dirname(projectPath);
|
|
240
|
+
try {
|
|
241
|
+
const entries = fs.readdirSync(dir);
|
|
242
|
+
for (const [type, markers] of Object.entries(PROJECT_MARKERS)) {
|
|
243
|
+
const manifestPattern = markers.manifest;
|
|
244
|
+
let hasManifest = false;
|
|
245
|
+
if (manifestPattern.includes('*')) {
|
|
246
|
+
const ext = manifestPattern.replace('*', '');
|
|
247
|
+
hasManifest = entries.some(e => e.endsWith(ext));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
hasManifest = entries.includes(manifestPattern);
|
|
251
|
+
}
|
|
252
|
+
if (hasManifest) {
|
|
253
|
+
const lockFile = markers.lockFiles.find(lf => entries.includes(lf));
|
|
254
|
+
return {
|
|
255
|
+
path: dir,
|
|
256
|
+
type,
|
|
257
|
+
lockFile: lockFile ? path.join(dir, lockFile) : undefined,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Permission denied or other error
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const scanProjectNative = async (project) => {
|
|
268
|
+
switch (project.type) {
|
|
269
|
+
case 'npm': return scanNpm(project.path);
|
|
270
|
+
case 'python': return scanPython(project.path);
|
|
271
|
+
case 'go': return scanGo(project.path);
|
|
272
|
+
case 'rust': return scanRust(project.path);
|
|
273
|
+
case 'ruby': return scanRuby(project.path);
|
|
274
|
+
case 'php': return scanPhp(project.path);
|
|
275
|
+
case 'java': return scanJava(project.path);
|
|
276
|
+
case 'dotnet': return scanDotnet(project.path);
|
|
277
|
+
default: return [];
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const getManifestContent = (project) => {
|
|
281
|
+
const manifestFiles = {
|
|
282
|
+
npm: 'package.json',
|
|
283
|
+
python: 'requirements.txt',
|
|
284
|
+
go: 'go.mod',
|
|
285
|
+
rust: 'Cargo.toml',
|
|
286
|
+
ruby: 'Gemfile',
|
|
287
|
+
php: 'composer.json',
|
|
288
|
+
java: 'pom.xml',
|
|
289
|
+
dotnet: '*.csproj',
|
|
290
|
+
};
|
|
291
|
+
const manifestName = manifestFiles[project.type];
|
|
292
|
+
if (manifestName.includes('*')) {
|
|
293
|
+
const ext = manifestName.replace('*', '');
|
|
294
|
+
const files = fs.readdirSync(project.path);
|
|
295
|
+
const match = files.find(f => f.endsWith(ext));
|
|
296
|
+
if (match) {
|
|
297
|
+
return fs.readFileSync(path.join(project.path, match), 'utf-8');
|
|
298
|
+
}
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
const manifestPath = path.join(project.path, manifestName);
|
|
302
|
+
if (fs.existsSync(manifestPath)) {
|
|
303
|
+
return fs.readFileSync(manifestPath, 'utf-8');
|
|
304
|
+
}
|
|
305
|
+
if (project.lockFile && fs.existsSync(project.lockFile)) {
|
|
306
|
+
return fs.readFileSync(project.lockFile, 'utf-8');
|
|
307
|
+
}
|
|
308
|
+
return '';
|
|
309
|
+
};
|
|
310
|
+
const scanProjectWithAI = async (project, options, nativeVulns = []) => {
|
|
311
|
+
const manifestContent = getManifestContent(project);
|
|
312
|
+
if (!manifestContent)
|
|
313
|
+
return [];
|
|
314
|
+
const prompt = getScanPrompt(project.type, manifestContent.slice(0, 10000), nativeVulns);
|
|
315
|
+
const llmOptions = { verbose: options.verbose };
|
|
316
|
+
try {
|
|
317
|
+
const issues = options.llm === 'claude'
|
|
318
|
+
? await analyzeWithClaude(project.path, prompt, llmOptions)
|
|
319
|
+
: await analyzeWithCodex(project.path, prompt, llmOptions);
|
|
320
|
+
return issues
|
|
321
|
+
.filter((issue) => issue.package || issue.filename)
|
|
322
|
+
.map((issue) => ({
|
|
323
|
+
package: issue.package || issue.filename || 'unknown',
|
|
324
|
+
version: issue.version || 'unknown',
|
|
325
|
+
severity: mapSeverity(issue.severity || 'medium'),
|
|
326
|
+
title: issue.title || issue.message || 'AI-detected vulnerability',
|
|
327
|
+
description: issue.description || issue.suggestion,
|
|
328
|
+
fixedIn: issue.fixedIn,
|
|
329
|
+
cve: issue.cve,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.log(chalk.gray(` AI analysis failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const mergeVulnerabilities = (native, ai) => {
|
|
338
|
+
const seen = new Set(native.map(v => `${v.package}@${v.version}`));
|
|
339
|
+
const unique = ai.filter(v => !seen.has(`${v.package}@${v.version}`));
|
|
340
|
+
return [...native, ...unique];
|
|
341
|
+
};
|
|
342
|
+
function scanNpm(projectPath) {
|
|
343
|
+
try {
|
|
344
|
+
// Check if package-lock.json exists, generate if missing
|
|
345
|
+
const lockPath = path.join(projectPath, 'package-lock.json');
|
|
346
|
+
const yarnLockPath = path.join(projectPath, 'yarn.lock');
|
|
347
|
+
const pnpmLockPath = path.join(projectPath, 'pnpm-lock.yaml');
|
|
348
|
+
if (!fs.existsSync(lockPath) && !fs.existsSync(yarnLockPath) && !fs.existsSync(pnpmLockPath)) {
|
|
349
|
+
console.log(chalk.gray(' No lock file found, skipping native audit (AI will analyze package.json)'));
|
|
350
|
+
// Skip npm install - it can hang on private registries or slow networks
|
|
351
|
+
// AI analysis will still work by reading package.json directly
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
const result = execSync('npm audit --json 2>&1 || true', {
|
|
355
|
+
cwd: projectPath,
|
|
356
|
+
encoding: 'utf-8',
|
|
357
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
358
|
+
});
|
|
359
|
+
// Parse result - handle both success and error cases
|
|
360
|
+
let audit = {};
|
|
361
|
+
try {
|
|
362
|
+
audit = JSON.parse(result);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// npm audit may return non-JSON on error
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
const vulnerabilities = [];
|
|
369
|
+
const seenPackages = new Set();
|
|
370
|
+
if (audit.vulnerabilities) {
|
|
371
|
+
for (const [pkg, data] of Object.entries(audit.vulnerabilities)) {
|
|
372
|
+
// Skip if we've already processed this package
|
|
373
|
+
if (seenPackages.has(pkg))
|
|
374
|
+
continue;
|
|
375
|
+
seenPackages.add(pkg);
|
|
376
|
+
const via = data.via?.[0];
|
|
377
|
+
// Extract CVE from via object - only use name if it looks like a CVE ID
|
|
378
|
+
let cve;
|
|
379
|
+
if (typeof via === 'object') {
|
|
380
|
+
if (via.cve) {
|
|
381
|
+
cve = via.cve;
|
|
382
|
+
}
|
|
383
|
+
else if (via.name && /^CVE-\d{4}-\d+$/.test(via.name)) {
|
|
384
|
+
cve = via.name;
|
|
385
|
+
}
|
|
386
|
+
else if (via.url) {
|
|
387
|
+
const match = via.url.match(/CVE-\d{4}-\d+/);
|
|
388
|
+
cve = match ? match[0] : undefined;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Get the actual installed version from nodes if available
|
|
392
|
+
const installedVersion = audit.metadata?.dependencies?.[pkg]?.version || data.range || 'unknown';
|
|
393
|
+
vulnerabilities.push({
|
|
394
|
+
package: pkg,
|
|
395
|
+
version: installedVersion,
|
|
396
|
+
severity: mapSeverity(data.severity),
|
|
397
|
+
title: typeof via === 'object' ? (via.title || 'Vulnerability found') : (via || 'Vulnerability found'),
|
|
398
|
+
description: typeof via === 'object' ? via.url : undefined,
|
|
399
|
+
fixedIn: data.fixAvailable?.version,
|
|
400
|
+
cve: cve,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return vulnerabilities;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function scanPython(projectPath) {
|
|
411
|
+
try {
|
|
412
|
+
// Try pip-audit first
|
|
413
|
+
const result = execSync('pip-audit --format json 2>/dev/null || python -m pip_audit --format json 2>/dev/null || true', {
|
|
414
|
+
cwd: projectPath,
|
|
415
|
+
encoding: 'utf-8',
|
|
416
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
417
|
+
});
|
|
418
|
+
if (!result.trim())
|
|
419
|
+
return [];
|
|
420
|
+
const audit = JSON.parse(result);
|
|
421
|
+
return audit.map((v) => ({
|
|
422
|
+
package: v.name,
|
|
423
|
+
version: v.version,
|
|
424
|
+
severity: mapSeverity(v.vulns?.[0]?.fix_versions ? 'high' : 'medium'),
|
|
425
|
+
title: v.vulns?.[0]?.id || 'Vulnerability found',
|
|
426
|
+
description: v.vulns?.[0]?.description,
|
|
427
|
+
fixedIn: v.vulns?.[0]?.fix_versions?.[0],
|
|
428
|
+
cve: v.vulns?.[0]?.id,
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function scanGo(projectPath) {
|
|
436
|
+
try {
|
|
437
|
+
const result = execSync('govulncheck -json ./... 2>/dev/null || true', {
|
|
438
|
+
cwd: projectPath,
|
|
439
|
+
encoding: 'utf-8',
|
|
440
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
441
|
+
});
|
|
442
|
+
if (!result.trim())
|
|
443
|
+
return [];
|
|
444
|
+
const vulnerabilities = [];
|
|
445
|
+
const lines = result.split('\n').filter(l => l.trim());
|
|
446
|
+
for (const line of lines) {
|
|
447
|
+
try {
|
|
448
|
+
const entry = JSON.parse(line);
|
|
449
|
+
if (entry.vulnerability) {
|
|
450
|
+
vulnerabilities.push({
|
|
451
|
+
package: entry.vulnerability.module_path || 'unknown',
|
|
452
|
+
version: entry.vulnerability.package_version || 'unknown',
|
|
453
|
+
severity: mapSeverity('high'),
|
|
454
|
+
title: entry.vulnerability.id || 'Vulnerability found',
|
|
455
|
+
description: entry.vulnerability.details,
|
|
456
|
+
cve: entry.vulnerability.id,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Skip non-JSON lines
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return vulnerabilities;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function scanRust(projectPath) {
|
|
471
|
+
try {
|
|
472
|
+
const result = execSync('cargo audit --json 2>/dev/null || true', {
|
|
473
|
+
cwd: projectPath,
|
|
474
|
+
encoding: 'utf-8',
|
|
475
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
476
|
+
});
|
|
477
|
+
if (!result.trim())
|
|
478
|
+
return [];
|
|
479
|
+
const audit = JSON.parse(result);
|
|
480
|
+
const vulnerabilities = [];
|
|
481
|
+
if (audit.vulnerabilities?.list) {
|
|
482
|
+
for (const v of audit.vulnerabilities.list) {
|
|
483
|
+
vulnerabilities.push({
|
|
484
|
+
package: v.package?.name || 'unknown',
|
|
485
|
+
version: v.package?.version || 'unknown',
|
|
486
|
+
severity: mapSeverity(v.advisory?.severity || 'medium'),
|
|
487
|
+
title: v.advisory?.title || 'Vulnerability found',
|
|
488
|
+
description: v.advisory?.description,
|
|
489
|
+
fixedIn: v.versions?.patched?.[0],
|
|
490
|
+
cve: v.advisory?.id,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return vulnerabilities;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function scanRuby(projectPath) {
|
|
501
|
+
try {
|
|
502
|
+
const result = execSync('bundle audit check --format json 2>/dev/null || true', {
|
|
503
|
+
cwd: projectPath,
|
|
504
|
+
encoding: 'utf-8',
|
|
505
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
506
|
+
});
|
|
507
|
+
if (!result.trim())
|
|
508
|
+
return [];
|
|
509
|
+
const audit = JSON.parse(result);
|
|
510
|
+
return (audit.results || []).map((v) => ({
|
|
511
|
+
package: v.gem?.name || 'unknown',
|
|
512
|
+
version: v.gem?.version || 'unknown',
|
|
513
|
+
severity: mapSeverity(v.advisory?.criticality || 'medium'),
|
|
514
|
+
title: v.advisory?.title || 'Vulnerability found',
|
|
515
|
+
description: v.advisory?.description,
|
|
516
|
+
fixedIn: v.advisory?.patched_versions?.[0],
|
|
517
|
+
cve: v.advisory?.cve,
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function scanPhp(projectPath) {
|
|
525
|
+
try {
|
|
526
|
+
const result = execSync('composer audit --format json 2>/dev/null || true', {
|
|
527
|
+
cwd: projectPath,
|
|
528
|
+
encoding: 'utf-8',
|
|
529
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
530
|
+
});
|
|
531
|
+
if (!result.trim())
|
|
532
|
+
return [];
|
|
533
|
+
const audit = JSON.parse(result);
|
|
534
|
+
const vulnerabilities = [];
|
|
535
|
+
if (audit.advisories) {
|
|
536
|
+
for (const [pkg, advisories] of Object.entries(audit.advisories)) {
|
|
537
|
+
for (const adv of advisories) {
|
|
538
|
+
vulnerabilities.push({
|
|
539
|
+
package: pkg,
|
|
540
|
+
version: adv.affectedVersions || 'unknown',
|
|
541
|
+
severity: mapSeverity('high'),
|
|
542
|
+
title: adv.title || 'Vulnerability found',
|
|
543
|
+
description: adv.link,
|
|
544
|
+
cve: adv.cve,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return vulnerabilities;
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function scanJava(projectPath) {
|
|
556
|
+
try {
|
|
557
|
+
// Try OWASP dependency-check if available
|
|
558
|
+
execSync('mvn org.owasp:dependency-check-maven:check -Dformat=JSON 2>/dev/null || true', {
|
|
559
|
+
cwd: projectPath,
|
|
560
|
+
encoding: 'utf-8',
|
|
561
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
562
|
+
timeout: 300000, // 5 minutes
|
|
563
|
+
});
|
|
564
|
+
// Parse dependency-check report if it exists
|
|
565
|
+
const reportPath = path.join(projectPath, 'target', 'dependency-check-report.json');
|
|
566
|
+
if (fs.existsSync(reportPath)) {
|
|
567
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
|
568
|
+
const vulnerabilities = [];
|
|
569
|
+
for (const dep of report.dependencies || []) {
|
|
570
|
+
for (const vuln of dep.vulnerabilities || []) {
|
|
571
|
+
vulnerabilities.push({
|
|
572
|
+
package: dep.fileName || 'unknown',
|
|
573
|
+
version: dep.version || 'unknown',
|
|
574
|
+
severity: mapSeverity(vuln.severity?.toLowerCase() || 'medium'),
|
|
575
|
+
title: vuln.name || 'Vulnerability found',
|
|
576
|
+
description: vuln.description,
|
|
577
|
+
cve: vuln.name,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return vulnerabilities;
|
|
582
|
+
}
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function scanDotnet(projectPath) {
|
|
590
|
+
try {
|
|
591
|
+
const result = execSync('dotnet list package --vulnerable --format json 2>/dev/null || true', {
|
|
592
|
+
cwd: projectPath,
|
|
593
|
+
encoding: 'utf-8',
|
|
594
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
595
|
+
});
|
|
596
|
+
if (!result.trim())
|
|
597
|
+
return [];
|
|
598
|
+
const audit = JSON.parse(result);
|
|
599
|
+
const vulnerabilities = [];
|
|
600
|
+
for (const project of audit.projects || []) {
|
|
601
|
+
for (const framework of project.frameworks || []) {
|
|
602
|
+
for (const pkg of framework.topLevelPackages || []) {
|
|
603
|
+
for (const vuln of pkg.vulnerabilities || []) {
|
|
604
|
+
vulnerabilities.push({
|
|
605
|
+
package: pkg.id || 'unknown',
|
|
606
|
+
version: pkg.resolvedVersion || 'unknown',
|
|
607
|
+
severity: mapSeverity(vuln.severity?.toLowerCase() || 'medium'),
|
|
608
|
+
title: vuln.advisoryurl || 'Vulnerability found',
|
|
609
|
+
description: vuln.advisoryurl,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return vulnerabilities;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function mapSeverity(severity) {
|
|
622
|
+
const s = severity?.toLowerCase() || 'medium';
|
|
623
|
+
if (s === 'critical')
|
|
624
|
+
return 'critical';
|
|
625
|
+
if (s === 'high')
|
|
626
|
+
return 'high';
|
|
627
|
+
if (s === 'moderate' || s === 'medium')
|
|
628
|
+
return 'medium';
|
|
629
|
+
return 'low';
|
|
630
|
+
}
|
|
631
|
+
const printScanResults = (results, rootPath) => {
|
|
632
|
+
const totalVulns = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
|
|
633
|
+
if (totalVulns === 0) {
|
|
634
|
+
console.log(chalk.green(' No vulnerabilities found in any project.'));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// Print per-project details
|
|
638
|
+
for (const result of results) {
|
|
639
|
+
if (result.vulnerabilities.length === 0)
|
|
640
|
+
continue;
|
|
641
|
+
const relativePath = path.relative(rootPath, result.projectPath) || '.';
|
|
642
|
+
console.log(chalk.bold(`\n 📦 ${result.projectType.toUpperCase()} - ${relativePath}`));
|
|
643
|
+
console.log(' ' + '─'.repeat(38));
|
|
644
|
+
const bySeverity = {
|
|
645
|
+
critical: result.vulnerabilities.filter(v => v.severity === 'critical'),
|
|
646
|
+
high: result.vulnerabilities.filter(v => v.severity === 'high'),
|
|
647
|
+
medium: result.vulnerabilities.filter(v => v.severity === 'medium'),
|
|
648
|
+
low: result.vulnerabilities.filter(v => v.severity === 'low'),
|
|
649
|
+
};
|
|
650
|
+
if (bySeverity.critical.length > 0) {
|
|
651
|
+
console.log(chalk.red.bold(`\n 🔴 Critical (${bySeverity.critical.length}):`));
|
|
652
|
+
bySeverity.critical.forEach(printVulnerability);
|
|
653
|
+
}
|
|
654
|
+
if (bySeverity.high.length > 0) {
|
|
655
|
+
console.log(chalk.red(`\n 🟠 High (${bySeverity.high.length}):`));
|
|
656
|
+
bySeverity.high.forEach(printVulnerability);
|
|
657
|
+
}
|
|
658
|
+
if (bySeverity.medium.length > 0) {
|
|
659
|
+
console.log(chalk.yellow(`\n 🟡 Medium (${bySeverity.medium.length}):`));
|
|
660
|
+
bySeverity.medium.forEach(printVulnerability);
|
|
661
|
+
}
|
|
662
|
+
if (bySeverity.low.length > 0) {
|
|
663
|
+
console.log(chalk.blue(`\n 🔵 Low (${bySeverity.low.length}):`));
|
|
664
|
+
bySeverity.low.forEach(printVulnerability);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const printVulnerability = (vuln) => {
|
|
669
|
+
console.log(` ${chalk.bold(vuln.package)}@${vuln.version}`);
|
|
670
|
+
console.log(chalk.gray(` ${vuln.title}`));
|
|
671
|
+
if (vuln.description && vuln.description !== vuln.title) {
|
|
672
|
+
const desc = vuln.description.length > 80 ? vuln.description.slice(0, 77) + '...' : vuln.description;
|
|
673
|
+
console.log(chalk.dim(` ${desc}`));
|
|
674
|
+
}
|
|
675
|
+
if (vuln.cve) {
|
|
676
|
+
console.log(chalk.cyan(` CVE: ${vuln.cve}`));
|
|
677
|
+
}
|
|
678
|
+
if (vuln.fixedIn) {
|
|
679
|
+
console.log(chalk.green(` 💡 Fix: upgrade to ${vuln.fixedIn}`));
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
const printFixSummary = (fixResult) => {
|
|
683
|
+
console.log(chalk.bold(`\n 📝 ${fixResult.summary}`));
|
|
684
|
+
if (fixResult.fixes.length === 0) {
|
|
685
|
+
console.log(chalk.gray(' No specific fixes generated'));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
console.log();
|
|
689
|
+
for (const fix of fixResult.fixes) {
|
|
690
|
+
const icon = fix.type === 'update' ? '⬆️' : fix.type === 'remove' ? '🗑️' : '➕';
|
|
691
|
+
const change = fix.type === 'update'
|
|
692
|
+
? `${fix.from} → ${fix.to}`
|
|
693
|
+
: fix.type === 'add'
|
|
694
|
+
? fix.version
|
|
695
|
+
: 'removed';
|
|
696
|
+
console.log(` ${icon} ${chalk.bold(fix.package)}: ${chalk.cyan(change)}`);
|
|
697
|
+
console.log(chalk.gray(` ${fix.reason}`));
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
//# sourceMappingURL=command.js.map
|