aura-security 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/deploy/AWS-DEPLOYMENT.md +358 -0
  4. package/deploy/terraform/main.tf +362 -0
  5. package/deploy/terraform/terraform.tfvars.example +6 -0
  6. package/dist/agents/base.d.ts +44 -0
  7. package/dist/agents/base.js +96 -0
  8. package/dist/agents/index.d.ts +14 -0
  9. package/dist/agents/index.js +17 -0
  10. package/dist/agents/policy/evaluator.d.ts +15 -0
  11. package/dist/agents/policy/evaluator.js +183 -0
  12. package/dist/agents/policy/index.d.ts +12 -0
  13. package/dist/agents/policy/index.js +15 -0
  14. package/dist/agents/policy/validator.d.ts +15 -0
  15. package/dist/agents/policy/validator.js +182 -0
  16. package/dist/agents/scanners/gitleaks.d.ts +14 -0
  17. package/dist/agents/scanners/gitleaks.js +155 -0
  18. package/dist/agents/scanners/grype.d.ts +14 -0
  19. package/dist/agents/scanners/grype.js +109 -0
  20. package/dist/agents/scanners/index.d.ts +15 -0
  21. package/dist/agents/scanners/index.js +27 -0
  22. package/dist/agents/scanners/npm-audit.d.ts +13 -0
  23. package/dist/agents/scanners/npm-audit.js +129 -0
  24. package/dist/agents/scanners/semgrep.d.ts +14 -0
  25. package/dist/agents/scanners/semgrep.js +131 -0
  26. package/dist/agents/scanners/trivy.d.ts +14 -0
  27. package/dist/agents/scanners/trivy.js +122 -0
  28. package/dist/agents/types.d.ts +137 -0
  29. package/dist/agents/types.js +91 -0
  30. package/dist/auditor/index.d.ts +3 -0
  31. package/dist/auditor/index.js +2 -0
  32. package/dist/auditor/pipeline.d.ts +19 -0
  33. package/dist/auditor/pipeline.js +240 -0
  34. package/dist/auditor/validator.d.ts +17 -0
  35. package/dist/auditor/validator.js +58 -0
  36. package/dist/aura/client.d.ts +29 -0
  37. package/dist/aura/client.js +125 -0
  38. package/dist/aura/index.d.ts +4 -0
  39. package/dist/aura/index.js +2 -0
  40. package/dist/aura/server.d.ts +45 -0
  41. package/dist/aura/server.js +343 -0
  42. package/dist/cli.d.ts +17 -0
  43. package/dist/cli.js +1433 -0
  44. package/dist/client/index.d.ts +41 -0
  45. package/dist/client/index.js +170 -0
  46. package/dist/compliance/index.d.ts +40 -0
  47. package/dist/compliance/index.js +292 -0
  48. package/dist/database/index.d.ts +77 -0
  49. package/dist/database/index.js +395 -0
  50. package/dist/index.d.ts +25 -0
  51. package/dist/index.js +762 -0
  52. package/dist/integrations/aura-scanner.d.ts +69 -0
  53. package/dist/integrations/aura-scanner.js +155 -0
  54. package/dist/integrations/aws-scanner.d.ts +63 -0
  55. package/dist/integrations/aws-scanner.js +624 -0
  56. package/dist/integrations/config.d.ts +69 -0
  57. package/dist/integrations/config.js +212 -0
  58. package/dist/integrations/github.d.ts +45 -0
  59. package/dist/integrations/github.js +201 -0
  60. package/dist/integrations/gitlab.d.ts +36 -0
  61. package/dist/integrations/gitlab.js +110 -0
  62. package/dist/integrations/index.d.ts +11 -0
  63. package/dist/integrations/index.js +11 -0
  64. package/dist/integrations/local-scanner.d.ts +146 -0
  65. package/dist/integrations/local-scanner.js +1654 -0
  66. package/dist/integrations/notifications.d.ts +99 -0
  67. package/dist/integrations/notifications.js +305 -0
  68. package/dist/integrations/scanners.d.ts +57 -0
  69. package/dist/integrations/scanners.js +217 -0
  70. package/dist/integrations/slop-scanner.d.ts +69 -0
  71. package/dist/integrations/slop-scanner.js +155 -0
  72. package/dist/integrations/webhook.d.ts +37 -0
  73. package/dist/integrations/webhook.js +256 -0
  74. package/dist/orchestrator/index.d.ts +72 -0
  75. package/dist/orchestrator/index.js +187 -0
  76. package/dist/output/index.d.ts +152 -0
  77. package/dist/output/index.js +399 -0
  78. package/dist/pipeline/index.d.ts +72 -0
  79. package/dist/pipeline/index.js +313 -0
  80. package/dist/sbom/index.d.ts +94 -0
  81. package/dist/sbom/index.js +298 -0
  82. package/dist/schemas/index.d.ts +2 -0
  83. package/dist/schemas/index.js +2 -0
  84. package/dist/schemas/input.schema.d.ts +87 -0
  85. package/dist/schemas/input.schema.js +44 -0
  86. package/dist/schemas/output.schema.d.ts +115 -0
  87. package/dist/schemas/output.schema.js +64 -0
  88. package/dist/serve-visualizer.d.ts +2 -0
  89. package/dist/serve-visualizer.js +78 -0
  90. package/dist/slop/client.d.ts +29 -0
  91. package/dist/slop/client.js +125 -0
  92. package/dist/slop/index.d.ts +4 -0
  93. package/dist/slop/index.js +2 -0
  94. package/dist/slop/server.d.ts +45 -0
  95. package/dist/slop/server.js +343 -0
  96. package/dist/types/events.d.ts +62 -0
  97. package/dist/types/events.js +2 -0
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/index.js +1 -0
  100. package/dist/visualizer/index.d.ts +4 -0
  101. package/dist/visualizer/index.js +181 -0
  102. package/dist/websocket/index.d.ts +88 -0
  103. package/dist/websocket/index.js +195 -0
  104. package/dist/zones/index.d.ts +7 -0
  105. package/dist/zones/index.js +7 -0
  106. package/dist/zones/manager.d.ts +101 -0
  107. package/dist/zones/manager.js +304 -0
  108. package/dist/zones/types.d.ts +78 -0
  109. package/dist/zones/types.js +33 -0
  110. package/package.json +84 -0
  111. package/visualizer/app.js +0 -0
  112. package/visualizer/index-minimal.html +1771 -0
  113. package/visualizer/index.html +2933 -0
  114. package/visualizer/landing.html +1328 -0
  115. package/visualizer/styles.css +0 -0
@@ -0,0 +1,1654 @@
1
+ // Local System Scanner - Audit the local machine for security issues
2
+ // Uses REAL security tools: gitleaks, trivy, semgrep, npm audit
3
+ // Scans: secrets in files, package vulnerabilities, git repos, env files
4
+ import { readFileSync, readdirSync, existsSync, statSync, mkdtempSync, rmSync } from 'fs';
5
+ import { join, extname, basename } from 'path';
6
+ import { execSync, spawnSync } from 'child_process';
7
+ import { tmpdir } from 'os';
8
+ // Check if a tool is available
9
+ function isToolAvailable(tool) {
10
+ try {
11
+ // gitleaks uses 'version' subcommand, not '--version' flag
12
+ const args = tool === 'gitleaks' ? ['version'] : ['--version'];
13
+ const result = spawnSync(tool, args, { encoding: 'utf-8', timeout: 5000 });
14
+ return result.status === 0;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ // Files to exclude from secret scanning (lock files, compiled outputs, etc.)
21
+ const SECRET_SCAN_EXCLUDE_FILES = [
22
+ 'package-lock.json',
23
+ 'yarn.lock',
24
+ 'pnpm-lock.yaml',
25
+ 'composer.lock',
26
+ 'Gemfile.lock',
27
+ 'Cargo.lock',
28
+ 'poetry.lock',
29
+ 'go.sum',
30
+ '.min.js',
31
+ '.min.css',
32
+ '.bundle.js',
33
+ '.chunk.js',
34
+ 'dist/',
35
+ 'build/',
36
+ '.next/',
37
+ 'coverage/'
38
+ ];
39
+ // Directories that often contain false positives (test data, scripts, examples)
40
+ const FALSE_POSITIVE_DIRS = [
41
+ 'node_modules',
42
+ '__tests__',
43
+ '__mocks__',
44
+ 'test/',
45
+ 'tests/',
46
+ 'spec/',
47
+ 'fixtures/',
48
+ 'examples/',
49
+ 'scripts/'
50
+ ];
51
+ // Rules to completely skip - too many false positives to be useful
52
+ // aws-secret-access-key matches any 40-char base64 string which catches SHA hashes,
53
+ // base64 content, UUIDs, etc. Real AWS detection uses AKIA prefix pattern instead.
54
+ const SKIP_RULES_ENTIRELY = [
55
+ 'aws-secret-access-key'
56
+ ];
57
+ // Rules that are somewhat prone to false positives
58
+ const FALSE_POSITIVE_RULES = [
59
+ 'generic-api-key'
60
+ ];
61
+ // Run gitleaks for secrets detection
62
+ function runGitleaks(targetPath) {
63
+ const findings = [];
64
+ if (!isToolAvailable('gitleaks')) {
65
+ console.log('[SCANNER] gitleaks not available, skipping');
66
+ return findings;
67
+ }
68
+ try {
69
+ console.log('[SCANNER] Running gitleaks...');
70
+ const reportPath = join(tmpdir(), `gitleaks-${Date.now()}.json`);
71
+ // Run gitleaks
72
+ spawnSync('gitleaks', [
73
+ 'detect',
74
+ '--source', targetPath,
75
+ '--report-format', 'json',
76
+ '--report-path', reportPath,
77
+ '--no-git',
78
+ '--exit-code', '0'
79
+ ], { encoding: 'utf-8', timeout: 120000 });
80
+ if (existsSync(reportPath)) {
81
+ const report = JSON.parse(readFileSync(reportPath, 'utf-8'));
82
+ let filteredCount = 0;
83
+ for (const finding of report) {
84
+ const ruleId = finding.RuleID?.toLowerCase() || '';
85
+ const filePath = finding.File.toLowerCase();
86
+ const fileName = basename(finding.File);
87
+ // Completely skip rules that have too many false positives
88
+ if (SKIP_RULES_ENTIRELY.includes(ruleId)) {
89
+ filteredCount++;
90
+ continue;
91
+ }
92
+ // Filter out false positives from lock files and generated files
93
+ const isExcludedFile = SECRET_SCAN_EXCLUDE_FILES.some(pattern => fileName === pattern || filePath.includes(pattern.toLowerCase()));
94
+ // Filter out findings from known false positive directories
95
+ const isInFalsePositiveDir = FALSE_POSITIVE_DIRS.some(dir => filePath.includes(dir.toLowerCase()));
96
+ // Filter out known false positive rules in certain file types
97
+ const isFalsePositiveRule = FALSE_POSITIVE_RULES.includes(ruleId) &&
98
+ (isExcludedFile || isInFalsePositiveDir);
99
+ if (isExcludedFile || isFalsePositiveRule) {
100
+ filteredCount++;
101
+ continue;
102
+ }
103
+ findings.push({
104
+ file: finding.File,
105
+ line: finding.StartLine,
106
+ type: finding.RuleID || finding.Description,
107
+ snippet: `[REDACTED - ${finding.Description}]`,
108
+ severity: finding.Entropy > 4.5 ? 'critical' : 'high'
109
+ });
110
+ }
111
+ // Cleanup
112
+ try {
113
+ rmSync(reportPath);
114
+ }
115
+ catch { }
116
+ if (filteredCount > 0) {
117
+ console.log(`[SCANNER] Filtered ${filteredCount} false positives from lock/generated files`);
118
+ }
119
+ }
120
+ console.log(`[SCANNER] gitleaks found ${findings.length} secrets`);
121
+ }
122
+ catch (err) {
123
+ console.error('[SCANNER] gitleaks error:', err);
124
+ }
125
+ return findings;
126
+ }
127
+ // Run trivy for vulnerability scanning
128
+ function runTrivy(targetPath) {
129
+ const findings = [];
130
+ if (!isToolAvailable('trivy')) {
131
+ console.log('[SCANNER] trivy not available, skipping');
132
+ return findings;
133
+ }
134
+ try {
135
+ console.log('[SCANNER] Running trivy...');
136
+ const result = spawnSync('trivy', [
137
+ 'fs',
138
+ '--format', 'json',
139
+ '--scanners', 'vuln',
140
+ '--quiet',
141
+ targetPath
142
+ ], { encoding: 'utf-8', timeout: 180000, maxBuffer: 50 * 1024 * 1024 });
143
+ if (result.stdout) {
144
+ const report = JSON.parse(result.stdout);
145
+ if (report.Results) {
146
+ for (const target of report.Results) {
147
+ if (target.Vulnerabilities) {
148
+ for (const vuln of target.Vulnerabilities) {
149
+ findings.push({
150
+ name: vuln.PkgName,
151
+ version: vuln.InstalledVersion,
152
+ vulnerabilities: 1,
153
+ severity: normalizeSeverity(vuln.Severity),
154
+ vulnId: vuln.VulnerabilityID,
155
+ title: vuln.Title,
156
+ fixedVersion: vuln.FixedVersion
157
+ });
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ console.log(`[SCANNER] trivy found ${findings.length} vulnerabilities`);
164
+ }
165
+ catch (err) {
166
+ console.error('[SCANNER] trivy error:', err);
167
+ }
168
+ return findings;
169
+ }
170
+ // Run semgrep for SAST
171
+ function runSemgrep(targetPath) {
172
+ const findings = [];
173
+ if (!isToolAvailable('semgrep')) {
174
+ console.log('[SCANNER] semgrep not available, skipping');
175
+ return findings;
176
+ }
177
+ try {
178
+ console.log('[SCANNER] Running semgrep...');
179
+ const result = spawnSync('semgrep', [
180
+ 'scan',
181
+ '--config', 'auto',
182
+ '--json',
183
+ '--quiet',
184
+ targetPath
185
+ ], { encoding: 'utf-8', timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
186
+ if (result.stdout) {
187
+ try {
188
+ const report = JSON.parse(result.stdout);
189
+ if (report.results) {
190
+ for (const finding of report.results) {
191
+ findings.push({
192
+ file: finding.path,
193
+ line: finding.start.line,
194
+ rule: finding.check_id,
195
+ message: finding.extra.message,
196
+ severity: finding.extra.severity || 'WARNING'
197
+ });
198
+ }
199
+ }
200
+ }
201
+ catch { }
202
+ }
203
+ console.log(`[SCANNER] semgrep found ${findings.length} issues`);
204
+ }
205
+ catch (err) {
206
+ console.error('[SCANNER] semgrep error:', err);
207
+ }
208
+ return findings;
209
+ }
210
+ // Run npm audit for package vulnerabilities
211
+ function runNpmAudit(targetPath) {
212
+ const findings = [];
213
+ const packageJsonPath = join(targetPath, 'package.json');
214
+ if (!existsSync(packageJsonPath)) {
215
+ return findings;
216
+ }
217
+ try {
218
+ console.log('[SCANNER] Running npm audit...');
219
+ const result = spawnSync('npm', ['audit', '--json'], {
220
+ cwd: targetPath,
221
+ encoding: 'utf-8',
222
+ timeout: 60000,
223
+ maxBuffer: 10 * 1024 * 1024,
224
+ shell: true
225
+ });
226
+ if (result.stdout) {
227
+ try {
228
+ const report = JSON.parse(result.stdout);
229
+ if (report.vulnerabilities) {
230
+ for (const [name, vuln] of Object.entries(report.vulnerabilities)) {
231
+ findings.push({
232
+ name,
233
+ version: vuln.range || 'unknown',
234
+ vulnerabilities: 1,
235
+ severity: normalizeSeverity(vuln.severity)
236
+ });
237
+ }
238
+ }
239
+ }
240
+ catch { }
241
+ }
242
+ console.log(`[SCANNER] npm audit found ${findings.length} vulnerabilities`);
243
+ }
244
+ catch (err) {
245
+ console.error('[SCANNER] npm audit error:', err);
246
+ }
247
+ return findings;
248
+ }
249
+ function normalizeSeverity(sev) {
250
+ const s = sev?.toLowerCase() || '';
251
+ if (s === 'critical' || s === 'crit')
252
+ return 'critical';
253
+ if (s === 'high' || s === 'h' || s === 'error')
254
+ return 'high';
255
+ if (s === 'medium' || s === 'med' || s === 'moderate' || s === 'warning')
256
+ return 'medium';
257
+ return 'low';
258
+ }
259
+ function runGrype(targetPath) {
260
+ const findings = [];
261
+ if (!isToolAvailable('grype')) {
262
+ console.log('[SCANNER] grype not available, skipping');
263
+ return findings;
264
+ }
265
+ try {
266
+ console.log('[SCANNER] Running grype...');
267
+ const result = spawnSync('grype', [
268
+ targetPath,
269
+ '-o', 'json',
270
+ '--quiet'
271
+ ], { encoding: 'utf-8', timeout: 180000, maxBuffer: 50 * 1024 * 1024 });
272
+ if (result.stdout) {
273
+ const report = JSON.parse(result.stdout);
274
+ if (report.matches) {
275
+ for (const match of report.matches) {
276
+ findings.push({
277
+ name: match.artifact.name,
278
+ version: match.artifact.version,
279
+ vulnerabilities: 1,
280
+ severity: normalizeSeverity(match.vulnerability.severity),
281
+ vulnId: match.vulnerability.id,
282
+ title: match.vulnerability.description || match.vulnerability.id,
283
+ fixedVersion: match.vulnerability.fix?.versions?.[0]
284
+ });
285
+ }
286
+ }
287
+ }
288
+ console.log(`[SCANNER] grype found ${findings.length} vulnerabilities`);
289
+ }
290
+ catch (err) {
291
+ console.error('[SCANNER] grype error:', err);
292
+ }
293
+ return findings;
294
+ }
295
+ function runPipAudit(targetPath) {
296
+ const findings = [];
297
+ // Check for Python project indicators
298
+ const hasPython = existsSync(join(targetPath, 'requirements.txt')) ||
299
+ existsSync(join(targetPath, 'pyproject.toml')) ||
300
+ existsSync(join(targetPath, 'setup.py')) ||
301
+ existsSync(join(targetPath, 'Pipfile'));
302
+ if (!hasPython) {
303
+ return findings;
304
+ }
305
+ if (!isToolAvailable('pip-audit')) {
306
+ console.log('[SCANNER] pip-audit not available, skipping Python scan');
307
+ return findings;
308
+ }
309
+ try {
310
+ console.log('[SCANNER] Running pip-audit...');
311
+ // Try requirements.txt first
312
+ const requirementsPath = join(targetPath, 'requirements.txt');
313
+ const args = existsSync(requirementsPath)
314
+ ? ['--requirement', requirementsPath, '--format', 'json']
315
+ : ['--format', 'json'];
316
+ const result = spawnSync('pip-audit', args, {
317
+ cwd: targetPath,
318
+ encoding: 'utf-8',
319
+ timeout: 120000,
320
+ maxBuffer: 10 * 1024 * 1024
321
+ });
322
+ if (result.stdout) {
323
+ try {
324
+ const report = JSON.parse(result.stdout);
325
+ for (const pkg of report) {
326
+ for (const vuln of pkg.vulns) {
327
+ findings.push({
328
+ name: pkg.name,
329
+ version: pkg.version,
330
+ vulnerabilities: 1,
331
+ severity: 'high', // pip-audit doesn't provide severity, default to high
332
+ vulnId: vuln.id,
333
+ title: vuln.description || vuln.id,
334
+ fixedVersion: vuln.fix_versions?.[0]
335
+ });
336
+ }
337
+ }
338
+ }
339
+ catch { }
340
+ }
341
+ console.log(`[SCANNER] pip-audit found ${findings.length} vulnerabilities`);
342
+ }
343
+ catch (err) {
344
+ console.error('[SCANNER] pip-audit error:', err);
345
+ }
346
+ return findings;
347
+ }
348
+ function runGoVulnCheck(targetPath) {
349
+ const findings = [];
350
+ // Check for Go project
351
+ const hasGo = existsSync(join(targetPath, 'go.mod'));
352
+ if (!hasGo) {
353
+ return findings;
354
+ }
355
+ if (!isToolAvailable('govulncheck')) {
356
+ console.log('[SCANNER] govulncheck not available, skipping Go scan');
357
+ return findings;
358
+ }
359
+ try {
360
+ console.log('[SCANNER] Running govulncheck...');
361
+ const result = spawnSync('govulncheck', ['-json', './...'], {
362
+ cwd: targetPath,
363
+ encoding: 'utf-8',
364
+ timeout: 180000,
365
+ maxBuffer: 50 * 1024 * 1024
366
+ });
367
+ if (result.stdout) {
368
+ // govulncheck outputs newline-delimited JSON
369
+ const lines = result.stdout.split('\n').filter(l => l.trim());
370
+ for (const line of lines) {
371
+ try {
372
+ const entry = JSON.parse(line);
373
+ if (entry.vulnerability) {
374
+ const vuln = entry.vulnerability;
375
+ for (const mod of vuln.modules || []) {
376
+ findings.push({
377
+ name: mod.path,
378
+ version: mod.found_version || 'unknown',
379
+ vulnerabilities: 1,
380
+ severity: 'high',
381
+ vulnId: vuln.osv?.id,
382
+ title: vuln.osv?.summary || vuln.osv?.id,
383
+ fixedVersion: mod.fixed_version
384
+ });
385
+ }
386
+ }
387
+ }
388
+ catch { }
389
+ }
390
+ }
391
+ console.log(`[SCANNER] govulncheck found ${findings.length} vulnerabilities`);
392
+ }
393
+ catch (err) {
394
+ console.error('[SCANNER] govulncheck error:', err);
395
+ }
396
+ return findings;
397
+ }
398
+ function runCargoAudit(targetPath) {
399
+ const findings = [];
400
+ // Check for Rust project
401
+ const hasRust = existsSync(join(targetPath, 'Cargo.toml'));
402
+ if (!hasRust) {
403
+ return findings;
404
+ }
405
+ if (!isToolAvailable('cargo-audit')) {
406
+ console.log('[SCANNER] cargo-audit not available, skipping Rust scan');
407
+ return findings;
408
+ }
409
+ try {
410
+ console.log('[SCANNER] Running cargo-audit...');
411
+ const result = spawnSync('cargo', ['audit', '--json'], {
412
+ cwd: targetPath,
413
+ encoding: 'utf-8',
414
+ timeout: 120000,
415
+ maxBuffer: 10 * 1024 * 1024
416
+ });
417
+ if (result.stdout) {
418
+ try {
419
+ const report = JSON.parse(result.stdout);
420
+ if (report.vulnerabilities?.list) {
421
+ for (const vuln of report.vulnerabilities.list) {
422
+ findings.push({
423
+ name: vuln.package.name,
424
+ version: vuln.package.version,
425
+ vulnerabilities: 1,
426
+ severity: normalizeSeverity(vuln.advisory.severity || 'high'),
427
+ vulnId: vuln.advisory.id,
428
+ title: vuln.advisory.title,
429
+ fixedVersion: vuln.versions?.patched?.[0]
430
+ });
431
+ }
432
+ }
433
+ }
434
+ catch { }
435
+ }
436
+ console.log(`[SCANNER] cargo-audit found ${findings.length} vulnerabilities`);
437
+ }
438
+ catch (err) {
439
+ console.error('[SCANNER] cargo-audit error:', err);
440
+ }
441
+ return findings;
442
+ }
443
+ function runBundleAudit(targetPath) {
444
+ const findings = [];
445
+ // Check for Ruby project
446
+ const hasRuby = existsSync(join(targetPath, 'Gemfile')) ||
447
+ existsSync(join(targetPath, 'Gemfile.lock'));
448
+ if (!hasRuby) {
449
+ return findings;
450
+ }
451
+ if (!isToolAvailable('bundle-audit')) {
452
+ console.log('[SCANNER] bundle-audit not available, skipping Ruby scan');
453
+ return findings;
454
+ }
455
+ try {
456
+ console.log('[SCANNER] Running bundle-audit...');
457
+ const result = spawnSync('bundle-audit', ['check', '--format', 'json'], {
458
+ cwd: targetPath,
459
+ encoding: 'utf-8',
460
+ timeout: 120000,
461
+ maxBuffer: 10 * 1024 * 1024
462
+ });
463
+ if (result.stdout) {
464
+ try {
465
+ const report = JSON.parse(result.stdout);
466
+ if (report.results) {
467
+ for (const vuln of report.results) {
468
+ findings.push({
469
+ name: vuln.gem.name,
470
+ version: vuln.gem.version,
471
+ vulnerabilities: 1,
472
+ severity: normalizeSeverity(vuln.advisory.criticality || 'high'),
473
+ vulnId: vuln.advisory.id,
474
+ title: vuln.advisory.title,
475
+ fixedVersion: vuln.advisory.patched_versions?.[0]
476
+ });
477
+ }
478
+ }
479
+ }
480
+ catch { }
481
+ }
482
+ console.log(`[SCANNER] bundle-audit found ${findings.length} vulnerabilities`);
483
+ }
484
+ catch (err) {
485
+ console.error('[SCANNER] bundle-audit error:', err);
486
+ }
487
+ return findings;
488
+ }
489
+ function runComposerAudit(targetPath) {
490
+ const findings = [];
491
+ // Check for PHP project
492
+ const hasPHP = existsSync(join(targetPath, 'composer.json'));
493
+ if (!hasPHP) {
494
+ return findings;
495
+ }
496
+ if (!isToolAvailable('composer')) {
497
+ console.log('[SCANNER] composer not available, skipping PHP scan');
498
+ return findings;
499
+ }
500
+ try {
501
+ console.log('[SCANNER] Running composer audit...');
502
+ const result = spawnSync('composer', ['audit', '--format', 'json'], {
503
+ cwd: targetPath,
504
+ encoding: 'utf-8',
505
+ timeout: 120000,
506
+ maxBuffer: 10 * 1024 * 1024,
507
+ shell: true
508
+ });
509
+ if (result.stdout) {
510
+ try {
511
+ const report = JSON.parse(result.stdout);
512
+ if (report.advisories) {
513
+ for (const [pkgName, advisories] of Object.entries(report.advisories)) {
514
+ for (const advisory of advisories) {
515
+ findings.push({
516
+ name: pkgName,
517
+ version: advisory.affectedVersions,
518
+ vulnerabilities: 1,
519
+ severity: normalizeSeverity(advisory.severity || 'high'),
520
+ vulnId: advisory.advisoryId,
521
+ title: advisory.title
522
+ });
523
+ }
524
+ }
525
+ }
526
+ }
527
+ catch { }
528
+ }
529
+ console.log(`[SCANNER] composer audit found ${findings.length} vulnerabilities`);
530
+ }
531
+ catch (err) {
532
+ console.error('[SCANNER] composer audit error:', err);
533
+ }
534
+ return findings;
535
+ }
536
+ function runCheckov(targetPath) {
537
+ const findings = [];
538
+ if (!isToolAvailable('checkov')) {
539
+ console.log('[SCANNER] checkov not available, skipping IaC scan');
540
+ return findings;
541
+ }
542
+ try {
543
+ console.log('[SCANNER] Running checkov...');
544
+ const result = spawnSync('checkov', [
545
+ '-d', targetPath,
546
+ '-o', 'json',
547
+ '--compact',
548
+ '--quiet'
549
+ ], { encoding: 'utf-8', timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
550
+ if (result.stdout) {
551
+ try {
552
+ // Checkov can output an array or single object
553
+ const output = result.stdout.trim();
554
+ const reports = output.startsWith('[')
555
+ ? JSON.parse(output)
556
+ : [JSON.parse(output)];
557
+ for (const report of reports) {
558
+ if (report.results?.failed_checks) {
559
+ for (const check of report.results.failed_checks) {
560
+ findings.push({
561
+ file: check.file_path,
562
+ resource: check.resource,
563
+ checkId: check.check_id,
564
+ checkType: check.check_type || 'general',
565
+ severity: normalizeSeverity(check.severity || 'medium'),
566
+ title: check.check_id,
567
+ guideline: check.guideline
568
+ });
569
+ }
570
+ }
571
+ }
572
+ }
573
+ catch { }
574
+ }
575
+ console.log(`[SCANNER] checkov found ${findings.length} IaC issues`);
576
+ }
577
+ catch (err) {
578
+ console.error('[SCANNER] checkov error:', err);
579
+ }
580
+ return findings;
581
+ }
582
+ function runHadolint(targetPath) {
583
+ const findings = [];
584
+ if (!isToolAvailable('hadolint')) {
585
+ console.log('[SCANNER] hadolint not available, skipping Dockerfile scan');
586
+ return findings;
587
+ }
588
+ // Find Dockerfiles
589
+ const dockerfiles = [];
590
+ const checkDir = (dir, depth = 0) => {
591
+ if (depth > 5)
592
+ return;
593
+ try {
594
+ const entries = readdirSync(dir, { withFileTypes: true });
595
+ for (const entry of entries) {
596
+ if (entry.name === 'node_modules' || entry.name === '.git')
597
+ continue;
598
+ const fullPath = join(dir, entry.name);
599
+ if (entry.isFile() && (entry.name === 'Dockerfile' || entry.name.startsWith('Dockerfile.'))) {
600
+ dockerfiles.push(fullPath);
601
+ }
602
+ else if (entry.isDirectory()) {
603
+ checkDir(fullPath, depth + 1);
604
+ }
605
+ }
606
+ }
607
+ catch { }
608
+ };
609
+ checkDir(targetPath);
610
+ if (dockerfiles.length === 0) {
611
+ return findings;
612
+ }
613
+ try {
614
+ console.log(`[SCANNER] Running hadolint on ${dockerfiles.length} Dockerfile(s)...`);
615
+ for (const dockerfile of dockerfiles) {
616
+ const result = spawnSync('hadolint', [dockerfile, '-f', 'json'], {
617
+ encoding: 'utf-8',
618
+ timeout: 30000
619
+ });
620
+ if (result.stdout) {
621
+ try {
622
+ const report = JSON.parse(result.stdout);
623
+ for (const issue of report) {
624
+ const severityMap = {
625
+ 'error': 'high',
626
+ 'warning': 'medium',
627
+ 'info': 'low',
628
+ 'style': 'low'
629
+ };
630
+ findings.push({
631
+ file: dockerfile,
632
+ line: issue.line,
633
+ code: issue.code,
634
+ message: issue.message,
635
+ severity: severityMap[issue.level] || 'medium'
636
+ });
637
+ }
638
+ }
639
+ catch { }
640
+ }
641
+ }
642
+ console.log(`[SCANNER] hadolint found ${findings.length} Dockerfile issues`);
643
+ }
644
+ catch (err) {
645
+ console.error('[SCANNER] hadolint error:', err);
646
+ }
647
+ return findings;
648
+ }
649
+ // Secret detection patterns - STRICT: only match actual values, not variable names
650
+ const SECRET_PATTERNS = [
651
+ // Only match when there's an actual value in quotes after = or :
652
+ { name: 'API Key', regex: /api[_-]?key\s*[=:]\s*['"]([A-Za-z0-9_\-]{20,})['"](?!\s*[,;]?\s*$)/gi, severity: 'high' },
653
+ { name: 'Secret Value', regex: /secret\s*[=:]\s*['"]([A-Za-z0-9_\-]{16,})['"](?!\s*[,;]?\s*$)/gi, severity: 'high' },
654
+ { name: 'Password', regex: /password\s*[=:]\s*['"]([^'"]{8,})['"](?!\s*[,;]?\s*$)/gi, severity: 'critical' },
655
+ // These are definitive patterns - actual secret formats
656
+ { name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g, severity: 'critical' },
657
+ // Note: AWS Secret Key pattern removed - too broad (matches any 40-char base64 string)
658
+ // Real AWS secrets are detected via AKIA access key pattern above
659
+ { name: 'Private Key', regex: /-----BEGIN\s+(RSA|EC|OPENSSH|PGP|ENCRYPTED)?\s*PRIVATE\s+KEY-----/g, severity: 'critical' },
660
+ { name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9_]{36,}/g, severity: 'critical' },
661
+ { name: 'GitLab Token', regex: /glpat-[A-Za-z0-9_-]{20,}/g, severity: 'critical' },
662
+ { name: 'Slack Token', regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g, severity: 'high' },
663
+ { name: 'Stripe Key', regex: /sk_live_[A-Za-z0-9]{24,}/g, severity: 'critical' },
664
+ { name: 'Stripe Test Key', regex: /sk_test_[A-Za-z0-9]{24,}/g, severity: 'medium' },
665
+ // Connection strings with actual credentials (user:pass@host pattern)
666
+ { name: 'Database URL', regex: /(mongodb|postgres|mysql|redis|amqp):\/\/[^:]+:[^@]+@[^\s"']+/gi, severity: 'critical' },
667
+ // Bearer tokens in actual use
668
+ { name: 'Bearer Token', regex: /["']Bearer\s+[A-Za-z0-9_\-\.]{20,}["']/gi, severity: 'high' },
669
+ // JWT tokens (actual tokens, not patterns)
670
+ { name: 'JWT Token', regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{20,}/g, severity: 'medium' },
671
+ // OpenAI API Key
672
+ { name: 'OpenAI Key', regex: /sk-[A-Za-z0-9]{32,}/g, severity: 'high' },
673
+ ];
674
+ // Files/patterns to SKIP for secret scanning (test files, examples, etc.)
675
+ const SKIP_SECRET_SCAN_PATTERNS = [
676
+ /example/i, /sample/i, /test/i, /mock/i, /fixture/i, /\.test\./i, /\.spec\./i,
677
+ /\.d\.ts$/, /\.map$/, /\.min\./
678
+ ];
679
+ // Files to scan for secrets
680
+ const FILES_TO_SCAN = [
681
+ '.env', '.env.local', '.env.development', '.env.production',
682
+ 'config.json', 'config.js', 'config.ts',
683
+ 'settings.json', 'settings.js',
684
+ 'credentials.json', 'secrets.json',
685
+ '.npmrc', '.yarnrc',
686
+ ];
687
+ // Service detection patterns - discover what technologies are used
688
+ const SERVICE_PATTERNS = [
689
+ // Databases
690
+ { name: 'MongoDB', id: 'mongodb', type: 'database', severity: 'high',
691
+ patterns: [/mongodb(\+srv)?:\/\//gi, /mongoose\.connect/gi, /MongoClient/gi, /MONGO_URI/gi, /MONGODB_URL/gi] },
692
+ { name: 'PostgreSQL', id: 'postgres', type: 'database', severity: 'high',
693
+ patterns: [/postgres(ql)?:\/\//gi, /pg\.connect/gi, /PG_HOST/gi, /POSTGRES_/gi, /DATABASE_URL.*postgres/gi] },
694
+ { name: 'MySQL', id: 'mysql', type: 'database', severity: 'high',
695
+ patterns: [/mysql:\/\//gi, /mysql\.createConnection/gi, /MYSQL_/gi] },
696
+ { name: 'Redis', id: 'redis', type: 'cache', severity: 'medium',
697
+ patterns: [/redis:\/\//gi, /createClient.*redis/gi, /REDIS_URL/gi, /REDIS_HOST/gi, /ioredis/gi] },
698
+ { name: 'SQLite', id: 'sqlite', type: 'database', severity: 'low',
699
+ patterns: [/sqlite3?/gi, /\.sqlite/gi, /better-sqlite/gi] },
700
+ { name: 'Elasticsearch', id: 'elasticsearch', type: 'database', severity: 'medium',
701
+ patterns: [/elasticsearch/gi, /ELASTIC_/gi, /@elastic\/elasticsearch/gi] },
702
+ // Cloud Services
703
+ { name: 'AWS', id: 'aws', type: 'cloud', severity: 'critical',
704
+ patterns: [/aws-sdk/gi, /AKIA[0-9A-Z]{16}/g, /AWS_ACCESS_KEY/gi, /AWS_SECRET/gi, /s3\.amazonaws/gi, /dynamodb/gi, /lambda/gi] },
705
+ { name: 'Google Cloud', id: 'gcp', type: 'cloud', severity: 'critical',
706
+ patterns: [/@google-cloud/gi, /GOOGLE_APPLICATION_CREDENTIALS/gi, /googleapis/gi, /firestore/gi, /gcloud/gi] },
707
+ { name: 'Azure', id: 'azure', type: 'cloud', severity: 'critical',
708
+ patterns: [/@azure/gi, /AZURE_/gi, /\.azure\./gi, /blob\.core\.windows/gi] },
709
+ { name: 'Firebase', id: 'firebase', type: 'cloud', severity: 'high',
710
+ patterns: [/firebase/gi, /FIREBASE_/gi, /firebaseConfig/gi] },
711
+ { name: 'Supabase', id: 'supabase', type: 'cloud', severity: 'high',
712
+ patterns: [/supabase/gi, /SUPABASE_/gi, /@supabase\/supabase-js/gi] },
713
+ // APIs & Services
714
+ { name: 'Stripe', id: 'stripe', type: 'api', severity: 'critical',
715
+ patterns: [/stripe/gi, /STRIPE_/gi, /sk_live_/gi, /sk_test_/gi, /pk_live_/gi] },
716
+ { name: 'Twilio', id: 'twilio', type: 'api', severity: 'high',
717
+ patterns: [/twilio/gi, /TWILIO_/gi] },
718
+ { name: 'SendGrid', id: 'sendgrid', type: 'api', severity: 'medium',
719
+ patterns: [/sendgrid/gi, /SENDGRID_/gi, /@sendgrid\/mail/gi] },
720
+ { name: 'OpenAI', id: 'openai', type: 'api', severity: 'high',
721
+ patterns: [/openai/gi, /OPENAI_API_KEY/gi, /sk-[a-zA-Z0-9]{32,}/gi] },
722
+ // Messaging
723
+ { name: 'RabbitMQ', id: 'rabbitmq', type: 'messaging', severity: 'medium',
724
+ patterns: [/amqp:\/\//gi, /rabbitmq/gi, /RABBITMQ_/gi] },
725
+ { name: 'Kafka', id: 'kafka', type: 'messaging', severity: 'medium',
726
+ patterns: [/kafkajs/gi, /KAFKA_/gi, /kafka\.connect/gi] },
727
+ // Auth
728
+ { name: 'Auth0', id: 'auth0', type: 'auth', severity: 'high',
729
+ patterns: [/auth0/gi, /AUTH0_/gi] },
730
+ { name: 'Okta', id: 'okta', type: 'auth', severity: 'high',
731
+ patterns: [/okta/gi, /OKTA_/gi] },
732
+ { name: 'JWT', id: 'jwt', type: 'auth', severity: 'medium',
733
+ patterns: [/jsonwebtoken/gi, /JWT_SECRET/gi, /eyJ[A-Za-z0-9_-]*\./gi] },
734
+ // Storage
735
+ { name: 'S3', id: 's3', type: 'storage', severity: 'high',
736
+ patterns: [/s3\.amazonaws/gi, /AWS_S3_/gi, /S3_BUCKET/gi] },
737
+ { name: 'Cloudflare R2', id: 'r2', type: 'storage', severity: 'high',
738
+ patterns: [/r2\.cloudflarestorage/gi, /R2_/gi] },
739
+ // Monitoring
740
+ { name: 'Datadog', id: 'datadog', type: 'monitoring', severity: 'low',
741
+ patterns: [/datadog/gi, /DD_API_KEY/gi, /dd-trace/gi] },
742
+ { name: 'Sentry', id: 'sentry', type: 'monitoring', severity: 'low',
743
+ patterns: [/sentry/gi, /SENTRY_DSN/gi, /@sentry/gi] },
744
+ { name: 'New Relic', id: 'newrelic', type: 'monitoring', severity: 'low',
745
+ patterns: [/newrelic/gi, /NEW_RELIC_/gi] },
746
+ ];
747
+ // Extensions to scan
748
+ const EXTENSIONS_TO_SCAN = ['.js', '.ts', '.json', '.yaml', '.yml', '.env', '.config', '.conf'];
749
+ export class LocalScanner {
750
+ config;
751
+ constructor(config) {
752
+ this.config = {
753
+ scanSecrets: true,
754
+ scanPackages: true,
755
+ scanGit: true,
756
+ scanEnvFiles: true,
757
+ maxDepth: 5,
758
+ excludePatterns: ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'],
759
+ ...config
760
+ };
761
+ }
762
+ async scan() {
763
+ const toolsUsed = [];
764
+ const languagesDetected = [];
765
+ const result = {
766
+ path: this.config.targetPath,
767
+ timestamp: new Date().toISOString(),
768
+ secrets: [],
769
+ packages: [],
770
+ sastFindings: [],
771
+ iacFindings: [],
772
+ dockerfileFindings: [],
773
+ gitInfo: null,
774
+ envFiles: [],
775
+ systemInfo: this.getSystemInfo(),
776
+ discoveredServices: [],
777
+ discoveredModules: [],
778
+ toolsUsed: [],
779
+ languagesDetected: []
780
+ };
781
+ console.log(`[SCANNER] Starting scan of: ${this.config.targetPath}`);
782
+ // ============ LANGUAGE DETECTION ============
783
+ const detectedLangs = this.detectLanguages(this.config.targetPath);
784
+ languagesDetected.push(...detectedLangs);
785
+ console.log(`[SCANNER] Detected languages: ${detectedLangs.join(', ') || 'none'}`);
786
+ // Filter by user-specified languages if provided
787
+ const shouldScanLang = (lang) => {
788
+ if (!this.config.languages || this.config.languages.length === 0)
789
+ return true;
790
+ return this.config.languages.includes(lang);
791
+ };
792
+ // Check if scanner should be used
793
+ const shouldUsescanner = (scanner) => {
794
+ if (!this.config.scanners || this.config.scanners.length === 0)
795
+ return true;
796
+ return this.config.scanners.includes(scanner);
797
+ };
798
+ // ============ SECRETS SCANNING ============
799
+ if (this.config.scanSecrets) {
800
+ // Try gitleaks first (real tool)
801
+ if (shouldUsescanner('gitleaks') && isToolAvailable('gitleaks')) {
802
+ console.log('[SCANNER] Using gitleaks for secrets detection');
803
+ const gitleaksFindings = runGitleaks(this.config.targetPath);
804
+ result.secrets.push(...gitleaksFindings);
805
+ toolsUsed.push('gitleaks');
806
+ }
807
+ else if (!this.config.scanners) {
808
+ // Fallback to regex-based scanning only if no specific scanners requested
809
+ console.log('[SCANNER] gitleaks not available, using regex patterns');
810
+ result.secrets = this.scanForSecrets(this.config.targetPath);
811
+ toolsUsed.push('regex-patterns');
812
+ }
813
+ }
814
+ // ============ PACKAGE VULNERABILITY SCANNING ============
815
+ if (this.config.scanPackages) {
816
+ // Grype - universal vulnerability scanner (runs on any project)
817
+ if (shouldUsescanner('grype') && isToolAvailable('grype')) {
818
+ console.log('[SCANNER] Using grype for universal vulnerability scanning');
819
+ const grypeFindings = runGrype(this.config.targetPath);
820
+ this.mergePackageFindings(result.packages, grypeFindings);
821
+ toolsUsed.push('grype');
822
+ }
823
+ // Trivy - another universal scanner
824
+ if (shouldUsescanner('trivy') && isToolAvailable('trivy')) {
825
+ console.log('[SCANNER] Using trivy for vulnerability scanning');
826
+ const trivyFindings = runTrivy(this.config.targetPath);
827
+ this.mergePackageFindings(result.packages, trivyFindings);
828
+ toolsUsed.push('trivy');
829
+ }
830
+ // JavaScript/Node.js - npm audit
831
+ if (shouldScanLang('js') && shouldUsescanner('npm-audit')) {
832
+ const npmAuditFindings = runNpmAudit(this.config.targetPath);
833
+ if (npmAuditFindings.length > 0) {
834
+ this.mergePackageFindings(result.packages, npmAuditFindings);
835
+ toolsUsed.push('npm-audit');
836
+ }
837
+ }
838
+ // Python - pip-audit
839
+ if (shouldScanLang('py') && shouldUsescanner('pip-audit')) {
840
+ const pipAuditFindings = runPipAudit(this.config.targetPath);
841
+ if (pipAuditFindings.length > 0) {
842
+ this.mergePackageFindings(result.packages, pipAuditFindings);
843
+ toolsUsed.push('pip-audit');
844
+ }
845
+ }
846
+ // Go - govulncheck
847
+ if (shouldScanLang('go') && shouldUsescanner('govulncheck')) {
848
+ const goFindings = runGoVulnCheck(this.config.targetPath);
849
+ if (goFindings.length > 0) {
850
+ this.mergePackageFindings(result.packages, goFindings);
851
+ toolsUsed.push('govulncheck');
852
+ }
853
+ }
854
+ // Rust - cargo-audit
855
+ if (shouldScanLang('rust') && shouldUsescanner('cargo-audit')) {
856
+ const cargoFindings = runCargoAudit(this.config.targetPath);
857
+ if (cargoFindings.length > 0) {
858
+ this.mergePackageFindings(result.packages, cargoFindings);
859
+ toolsUsed.push('cargo-audit');
860
+ }
861
+ }
862
+ // Ruby - bundle-audit
863
+ if (shouldScanLang('ruby') && shouldUsescanner('bundle-audit')) {
864
+ const rubyFindings = runBundleAudit(this.config.targetPath);
865
+ if (rubyFindings.length > 0) {
866
+ this.mergePackageFindings(result.packages, rubyFindings);
867
+ toolsUsed.push('bundle-audit');
868
+ }
869
+ }
870
+ // PHP - composer audit
871
+ if (shouldScanLang('php') && shouldUsescanner('composer')) {
872
+ const phpFindings = runComposerAudit(this.config.targetPath);
873
+ if (phpFindings.length > 0) {
874
+ this.mergePackageFindings(result.packages, phpFindings);
875
+ toolsUsed.push('composer-audit');
876
+ }
877
+ }
878
+ // Fallback if no real tools ran
879
+ if (result.packages.length === 0 && !toolsUsed.some(t => ['grype', 'trivy', 'npm-audit', 'pip-audit', 'govulncheck', 'cargo-audit', 'bundle-audit', 'composer-audit'].includes(t))) {
880
+ console.log('[SCANNER] No package scanners available, using basic scan');
881
+ result.packages = await this.scanPackages(this.config.targetPath);
882
+ toolsUsed.push('basic-package-scan');
883
+ }
884
+ }
885
+ // ============ SAST SCANNING ============
886
+ if (shouldUsescanner('semgrep') && isToolAvailable('semgrep')) {
887
+ console.log('[SCANNER] Using semgrep for SAST analysis');
888
+ const semgrepFindings = runSemgrep(this.config.targetPath);
889
+ result.sastFindings = semgrepFindings;
890
+ toolsUsed.push('semgrep');
891
+ }
892
+ else {
893
+ console.log('[SCANNER] semgrep not available, skipping SAST');
894
+ }
895
+ // ============ IAC SCANNING ============
896
+ if (this.config.scanIaC !== false) {
897
+ if (shouldUsescanner('checkov') && isToolAvailable('checkov')) {
898
+ console.log('[SCANNER] Using checkov for IaC scanning');
899
+ const checkovFindings = runCheckov(this.config.targetPath);
900
+ result.iacFindings = checkovFindings;
901
+ toolsUsed.push('checkov');
902
+ }
903
+ }
904
+ // ============ DOCKERFILE SCANNING ============
905
+ if (this.config.scanDockerfiles !== false) {
906
+ if (shouldUsescanner('hadolint') && isToolAvailable('hadolint')) {
907
+ console.log('[SCANNER] Using hadolint for Dockerfile scanning');
908
+ const hadolintFindings = runHadolint(this.config.targetPath);
909
+ result.dockerfileFindings = hadolintFindings;
910
+ toolsUsed.push('hadolint');
911
+ }
912
+ }
913
+ // ============ GIT INFO ============
914
+ if (this.config.scanGit) {
915
+ result.gitInfo = this.getGitInfo(this.config.targetPath);
916
+ }
917
+ // ============ ENV FILES ============
918
+ if (this.config.scanEnvFiles) {
919
+ result.envFiles = this.scanEnvFiles(this.config.targetPath);
920
+ }
921
+ // ============ SERVICE DISCOVERY ============
922
+ // Always discover services - this builds out the map
923
+ result.discoveredServices = this.discoverServices(this.config.targetPath);
924
+ // ============ MODULE DISCOVERY ============
925
+ // Discover code modules/directories - this maps the codebase structure
926
+ result.discoveredModules = this.discoverModules(this.config.targetPath);
927
+ result.toolsUsed = toolsUsed;
928
+ result.languagesDetected = languagesDetected;
929
+ console.log(`[SCANNER] Scan complete. Tools used: ${toolsUsed.join(', ')}`);
930
+ console.log(`[SCANNER] Found: ${result.secrets.length} secrets, ${result.packages.length} package vulns, ${result.sastFindings.length} SAST, ${result.iacFindings.length} IaC, ${result.dockerfileFindings.length} Dockerfile issues`);
931
+ return result;
932
+ }
933
+ // Detect languages in the project
934
+ detectLanguages(dir) {
935
+ const languages = new Set();
936
+ // Check for project files
937
+ if (existsSync(join(dir, 'package.json')))
938
+ languages.add('js');
939
+ if (existsSync(join(dir, 'requirements.txt')) || existsSync(join(dir, 'pyproject.toml')) || existsSync(join(dir, 'setup.py')) || existsSync(join(dir, 'Pipfile')))
940
+ languages.add('py');
941
+ if (existsSync(join(dir, 'go.mod')))
942
+ languages.add('go');
943
+ if (existsSync(join(dir, 'Cargo.toml')))
944
+ languages.add('rust');
945
+ if (existsSync(join(dir, 'Gemfile')))
946
+ languages.add('ruby');
947
+ if (existsSync(join(dir, 'composer.json')))
948
+ languages.add('php');
949
+ if (existsSync(join(dir, 'pom.xml')) || existsSync(join(dir, 'build.gradle')))
950
+ languages.add('java');
951
+ if (existsSync(join(dir, '*.csproj')) || existsSync(join(dir, '*.sln')))
952
+ languages.add('csharp');
953
+ return Array.from(languages);
954
+ }
955
+ // Merge package findings avoiding duplicates
956
+ mergePackageFindings(existing, newFindings) {
957
+ for (const finding of newFindings) {
958
+ const isDuplicate = existing.some(p => p.name === finding.name &&
959
+ p.version === finding.version &&
960
+ (p.vulnId === finding.vulnId || (!p.vulnId && !finding.vulnId)));
961
+ if (!isDuplicate) {
962
+ existing.push(finding);
963
+ }
964
+ }
965
+ }
966
+ // Discover what services/technologies are used in the codebase
967
+ discoverServices(dir) {
968
+ const discovered = new Map();
969
+ // Scan package.json for dependencies
970
+ const pkgPath = join(dir, 'package.json');
971
+ if (existsSync(pkgPath)) {
972
+ try {
973
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
974
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
975
+ for (const pattern of SERVICE_PATTERNS) {
976
+ for (const depName of Object.keys(allDeps)) {
977
+ for (const regex of pattern.patterns) {
978
+ regex.lastIndex = 0;
979
+ if (regex.test(depName)) {
980
+ if (!discovered.has(pattern.id)) {
981
+ discovered.set(pattern.id, {
982
+ id: pattern.id,
983
+ name: pattern.name,
984
+ type: pattern.type,
985
+ source: 'package.json',
986
+ severity: pattern.severity
987
+ });
988
+ }
989
+ break;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ }
995
+ catch { /* ignore */ }
996
+ }
997
+ // Scan source files for service usage
998
+ this.scanFilesForServices(dir, discovered);
999
+ return Array.from(discovered.values());
1000
+ }
1001
+ scanFilesForServices(dir, discovered, depth = 0) {
1002
+ if (depth > (this.config.maxDepth || 5))
1003
+ return;
1004
+ if (!existsSync(dir))
1005
+ return;
1006
+ try {
1007
+ const entries = readdirSync(dir, { withFileTypes: true });
1008
+ for (const entry of entries) {
1009
+ const fullPath = join(dir, entry.name);
1010
+ if (this.config.excludePatterns?.some(p => entry.name.includes(p))) {
1011
+ continue;
1012
+ }
1013
+ if (entry.isDirectory()) {
1014
+ this.scanFilesForServices(fullPath, discovered, depth + 1);
1015
+ }
1016
+ else if (entry.isFile()) {
1017
+ const ext = extname(entry.name).toLowerCase();
1018
+ if (EXTENSIONS_TO_SCAN.includes(ext) || entry.name.startsWith('.env')) {
1019
+ this.checkFileForServices(fullPath, discovered);
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ catch { /* ignore permission errors */ }
1025
+ }
1026
+ checkFileForServices(filePath, discovered) {
1027
+ try {
1028
+ // Skip the scanner's own pattern file to avoid false positives
1029
+ if (filePath.includes('local-scanner') || filePath.includes('scanners.ts')) {
1030
+ return;
1031
+ }
1032
+ const stats = statSync(filePath);
1033
+ if (stats.size > 1024 * 1024)
1034
+ return; // Skip large files
1035
+ const content = readFileSync(filePath, 'utf-8');
1036
+ for (const pattern of SERVICE_PATTERNS) {
1037
+ if (discovered.has(pattern.id))
1038
+ continue; // Already found
1039
+ for (const regex of pattern.patterns) {
1040
+ regex.lastIndex = 0;
1041
+ const match = regex.exec(content);
1042
+ if (match) {
1043
+ discovered.set(pattern.id, {
1044
+ id: pattern.id,
1045
+ name: pattern.name,
1046
+ type: pattern.type,
1047
+ source: filePath,
1048
+ connectionInfo: this.maskConnectionString(match[0]),
1049
+ severity: pattern.severity
1050
+ });
1051
+ break;
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ catch { /* ignore */ }
1057
+ }
1058
+ maskConnectionString(str) {
1059
+ // Mask passwords and secrets in connection strings
1060
+ return str
1061
+ .replace(/:\/\/[^:]+:[^@]+@/, '://***:***@')
1062
+ .replace(/password[=:][^&\s]+/gi, 'password=***')
1063
+ .replace(/secret[=:][^&\s]+/gi, 'secret=***');
1064
+ }
1065
+ // Discover code modules/directories in the codebase
1066
+ discoverModules(dir) {
1067
+ const modules = [];
1068
+ // Common module directory patterns and their types
1069
+ const MODULE_PATTERNS = [
1070
+ { pattern: /^src$/i, type: 'source', name: 'Source' },
1071
+ { pattern: /^(components?|ui)$/i, type: 'component', name: 'Components' },
1072
+ { pattern: /^(services?|core)$/i, type: 'service', name: 'Services' },
1073
+ { pattern: /^(api|routes?|endpoints?|controllers?)$/i, type: 'api', name: 'API' },
1074
+ { pattern: /^(lib|utils?|helpers?|common)$/i, type: 'lib', name: 'Library' },
1075
+ { pattern: /^(config|settings?)$/i, type: 'config', name: 'Config' },
1076
+ { pattern: /^(tests?|__tests__|spec)$/i, type: 'test', name: 'Tests' },
1077
+ { pattern: /^(infra|infrastructure|deploy|k8s|terraform)$/i, type: 'infra', name: 'Infrastructure' },
1078
+ { pattern: /^(docs?|documentation)$/i, type: 'docs', name: 'Documentation' },
1079
+ { pattern: /^(models?|entities|schemas?)$/i, type: 'source', name: 'Models' },
1080
+ { pattern: /^(middleware|interceptors?)$/i, type: 'service', name: 'Middleware' },
1081
+ { pattern: /^(hooks|composables)$/i, type: 'component', name: 'Hooks' },
1082
+ { pattern: /^(store|state|redux|contexts?)$/i, type: 'service', name: 'State' },
1083
+ { pattern: /^(pages?|views?|screens?)$/i, type: 'component', name: 'Pages' },
1084
+ { pattern: /^(assets|public|static)$/i, type: 'docs', name: 'Assets' },
1085
+ { pattern: /^(types?|interfaces|dtos?)$/i, type: 'source', name: 'Types' },
1086
+ ];
1087
+ try {
1088
+ const entries = readdirSync(dir, { withFileTypes: true });
1089
+ for (const entry of entries) {
1090
+ if (!entry.isDirectory())
1091
+ continue;
1092
+ // Skip excluded directories
1093
+ if (this.config.excludePatterns?.some(p => entry.name.includes(p))) {
1094
+ continue;
1095
+ }
1096
+ const fullPath = join(dir, entry.name);
1097
+ // Check if this matches a known module pattern
1098
+ for (const mp of MODULE_PATTERNS) {
1099
+ if (mp.pattern.test(entry.name)) {
1100
+ const moduleInfo = this.analyzeModule(fullPath, entry.name, mp.type, mp.name);
1101
+ if (moduleInfo.fileCount > 0) {
1102
+ modules.push(moduleInfo);
1103
+ }
1104
+ break;
1105
+ }
1106
+ }
1107
+ // Also check one level deeper for nested module structures (e.g., src/components)
1108
+ if (entry.name === 'src' || entry.name === 'app' || entry.name === 'packages') {
1109
+ const nestedModules = this.scanNestedModules(fullPath, MODULE_PATTERNS);
1110
+ modules.push(...nestedModules);
1111
+ }
1112
+ }
1113
+ // If we found an src module, also scan it for submodules
1114
+ const srcModule = modules.find(m => m.id === 'src');
1115
+ if (!srcModule) {
1116
+ // If there's no src directory, treat root level as source
1117
+ const rootFiles = this.getCodeFiles(dir);
1118
+ if (rootFiles.length > 0) {
1119
+ modules.push({
1120
+ id: 'root',
1121
+ name: 'Root',
1122
+ path: dir,
1123
+ type: 'source',
1124
+ fileCount: rootFiles.length,
1125
+ files: rootFiles.slice(0, 10), // Limit to first 10
1126
+ imports: [],
1127
+ exports: []
1128
+ });
1129
+ }
1130
+ }
1131
+ }
1132
+ catch { /* ignore permission errors */ }
1133
+ return modules;
1134
+ }
1135
+ scanNestedModules(dir, patterns) {
1136
+ const modules = [];
1137
+ try {
1138
+ const entries = readdirSync(dir, { withFileTypes: true });
1139
+ for (const entry of entries) {
1140
+ if (!entry.isDirectory())
1141
+ continue;
1142
+ if (this.config.excludePatterns?.some(p => entry.name.includes(p)))
1143
+ continue;
1144
+ const fullPath = join(dir, entry.name);
1145
+ for (const mp of patterns) {
1146
+ if (mp.pattern.test(entry.name)) {
1147
+ const moduleInfo = this.analyzeModule(fullPath, entry.name, mp.type, mp.name);
1148
+ if (moduleInfo.fileCount > 0) {
1149
+ modules.push(moduleInfo);
1150
+ }
1151
+ break;
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ catch { /* ignore */ }
1157
+ return modules;
1158
+ }
1159
+ analyzeModule(dir, dirName, type, displayName) {
1160
+ const files = this.getCodeFiles(dir);
1161
+ const imports = new Set();
1162
+ const exports = new Set();
1163
+ // Analyze a sample of files for imports/exports
1164
+ const sampleFiles = files.slice(0, 5);
1165
+ for (const file of sampleFiles) {
1166
+ try {
1167
+ const content = readFileSync(join(dir, file), 'utf-8');
1168
+ // Extract imports (basic regex for common patterns)
1169
+ const importMatches = content.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g);
1170
+ for (const match of importMatches) {
1171
+ const importPath = match[1];
1172
+ // Only track relative imports to other modules
1173
+ if (importPath.startsWith('.') || importPath.startsWith('@/')) {
1174
+ const moduleName = importPath.split('/')[1] || importPath.split('/')[0];
1175
+ if (moduleName && !moduleName.startsWith('.')) {
1176
+ imports.add(moduleName.replace('@/', ''));
1177
+ }
1178
+ }
1179
+ }
1180
+ // Extract exports (basic regex)
1181
+ const exportMatches = content.matchAll(/export\s+(const|function|class|interface|type|default)\s+(\w+)/g);
1182
+ for (const match of exportMatches) {
1183
+ exports.add(match[2]);
1184
+ }
1185
+ }
1186
+ catch { /* ignore */ }
1187
+ }
1188
+ return {
1189
+ id: dirName.toLowerCase(),
1190
+ name: displayName || dirName.charAt(0).toUpperCase() + dirName.slice(1),
1191
+ path: dir,
1192
+ type,
1193
+ fileCount: files.length,
1194
+ files: files.slice(0, 10), // Limit file list
1195
+ imports: Array.from(imports).slice(0, 10),
1196
+ exports: Array.from(exports).slice(0, 10)
1197
+ };
1198
+ }
1199
+ getCodeFiles(dir, depth = 0) {
1200
+ const files = [];
1201
+ if (depth > 3)
1202
+ return files; // Limit recursion
1203
+ try {
1204
+ const entries = readdirSync(dir, { withFileTypes: true });
1205
+ for (const entry of entries) {
1206
+ if (this.config.excludePatterns?.some(p => entry.name.includes(p)))
1207
+ continue;
1208
+ if (entry.isFile()) {
1209
+ const ext = extname(entry.name).toLowerCase();
1210
+ if (['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte', '.py', '.go', '.rs', '.java', '.cs'].includes(ext)) {
1211
+ files.push(entry.name);
1212
+ }
1213
+ }
1214
+ else if (entry.isDirectory() && depth < 3) {
1215
+ const subFiles = this.getCodeFiles(join(dir, entry.name), depth + 1);
1216
+ files.push(...subFiles.map(f => join(entry.name, f)));
1217
+ }
1218
+ }
1219
+ }
1220
+ catch { /* ignore */ }
1221
+ return files;
1222
+ }
1223
+ getSystemInfo() {
1224
+ return {
1225
+ platform: process.platform,
1226
+ hostname: process.env.COMPUTERNAME || process.env.HOSTNAME || 'unknown',
1227
+ user: process.env.USERNAME || process.env.USER || 'unknown',
1228
+ nodeVersion: process.version,
1229
+ cwd: process.cwd()
1230
+ };
1231
+ }
1232
+ scanForSecrets(dir, depth = 0) {
1233
+ const findings = [];
1234
+ if (depth > (this.config.maxDepth || 5))
1235
+ return findings;
1236
+ if (!existsSync(dir))
1237
+ return findings;
1238
+ try {
1239
+ const entries = readdirSync(dir, { withFileTypes: true });
1240
+ for (const entry of entries) {
1241
+ const fullPath = join(dir, entry.name);
1242
+ // Skip excluded patterns
1243
+ if (this.config.excludePatterns?.some(p => entry.name.includes(p))) {
1244
+ continue;
1245
+ }
1246
+ if (entry.isDirectory()) {
1247
+ findings.push(...this.scanForSecrets(fullPath, depth + 1));
1248
+ }
1249
+ else if (entry.isFile()) {
1250
+ // Check if file should be scanned
1251
+ const ext = extname(entry.name).toLowerCase();
1252
+ const shouldScan = FILES_TO_SCAN.includes(entry.name) ||
1253
+ EXTENSIONS_TO_SCAN.includes(ext);
1254
+ if (shouldScan) {
1255
+ findings.push(...this.scanFile(fullPath));
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+ catch {
1261
+ // Ignore permission errors
1262
+ }
1263
+ return findings;
1264
+ }
1265
+ scanFile(filePath) {
1266
+ const findings = [];
1267
+ try {
1268
+ // Skip test/example/sample files to reduce false positives
1269
+ const fileName = basename(filePath);
1270
+ if (SKIP_SECRET_SCAN_PATTERNS.some(pattern => pattern.test(fileName) || pattern.test(filePath))) {
1271
+ return findings;
1272
+ }
1273
+ // Skip the scanner's own files
1274
+ if (filePath.includes('local-scanner') || filePath.includes('scanners.ts') || filePath.includes('pipeline')) {
1275
+ return findings;
1276
+ }
1277
+ const stats = statSync(filePath);
1278
+ // Skip large files (> 1MB)
1279
+ if (stats.size > 1024 * 1024)
1280
+ return findings;
1281
+ const content = readFileSync(filePath, 'utf-8');
1282
+ const lines = content.split('\n');
1283
+ for (let i = 0; i < lines.length; i++) {
1284
+ const line = lines[i];
1285
+ for (const pattern of SECRET_PATTERNS) {
1286
+ pattern.regex.lastIndex = 0;
1287
+ const match = pattern.regex.exec(line);
1288
+ if (match) {
1289
+ findings.push({
1290
+ file: filePath,
1291
+ line: i + 1,
1292
+ type: pattern.name,
1293
+ snippet: this.maskSecret(line.trim()),
1294
+ severity: pattern.severity
1295
+ });
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+ catch {
1301
+ // Ignore read errors
1302
+ }
1303
+ return findings;
1304
+ }
1305
+ maskSecret(text) {
1306
+ // Mask secrets in the snippet to avoid exposing them
1307
+ return text.replace(/(['"])[^'"]{8,}(['"])/g, '$1***MASKED***$2');
1308
+ }
1309
+ async scanPackages(dir) {
1310
+ const findings = [];
1311
+ const packageJsonPath = join(dir, 'package.json');
1312
+ if (!existsSync(packageJsonPath))
1313
+ return findings;
1314
+ try {
1315
+ // Try to run npm audit
1316
+ const auditOutput = execSync('npm audit --json 2>/dev/null || echo "{}"', {
1317
+ cwd: dir,
1318
+ encoding: 'utf-8',
1319
+ timeout: 30000
1320
+ });
1321
+ const auditData = JSON.parse(auditOutput);
1322
+ const vulnerabilities = auditData.vulnerabilities || {};
1323
+ for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
1324
+ const v = vuln;
1325
+ findings.push({
1326
+ name: pkgName,
1327
+ version: v.range || 'unknown',
1328
+ vulnerabilities: 1,
1329
+ severity: this.normalizeSeverity(v.severity || 'medium')
1330
+ });
1331
+ }
1332
+ }
1333
+ catch {
1334
+ // npm audit failed, try to read package.json for outdated packages
1335
+ try {
1336
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
1337
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1338
+ for (const [name, version] of Object.entries(deps)) {
1339
+ // Flag very old or pinned versions as potential issues
1340
+ if (typeof version === 'string' && !version.startsWith('^') && !version.startsWith('~')) {
1341
+ findings.push({
1342
+ name,
1343
+ version: version,
1344
+ vulnerabilities: 0,
1345
+ severity: 'low'
1346
+ });
1347
+ }
1348
+ }
1349
+ }
1350
+ catch {
1351
+ // Ignore
1352
+ }
1353
+ }
1354
+ return findings;
1355
+ }
1356
+ normalizeSeverity(sev) {
1357
+ const s = sev.toLowerCase();
1358
+ if (s === 'critical' || s === 'crit')
1359
+ return 'critical';
1360
+ if (s === 'high' || s === 'h')
1361
+ return 'high';
1362
+ if (s === 'medium' || s === 'med' || s === 'moderate')
1363
+ return 'medium';
1364
+ return 'low';
1365
+ }
1366
+ getGitInfo(dir) {
1367
+ const gitDir = join(dir, '.git');
1368
+ if (!existsSync(gitDir))
1369
+ return null;
1370
+ try {
1371
+ const branch = execSync('git branch --show-current', { cwd: dir, encoding: 'utf-8' }).trim();
1372
+ let remoteUrl;
1373
+ let uncommittedChanges = 0;
1374
+ let lastCommit;
1375
+ try {
1376
+ remoteUrl = execSync('git remote get-url origin', { cwd: dir, encoding: 'utf-8' }).trim();
1377
+ }
1378
+ catch { /* no remote */ }
1379
+ try {
1380
+ const status = execSync('git status --porcelain', { cwd: dir, encoding: 'utf-8' });
1381
+ uncommittedChanges = status.split('\n').filter(l => l.trim()).length;
1382
+ }
1383
+ catch { /* ignore */ }
1384
+ try {
1385
+ lastCommit = execSync('git log -1 --format="%H %s"', { cwd: dir, encoding: 'utf-8' }).trim();
1386
+ }
1387
+ catch { /* ignore */ }
1388
+ return { branch, remoteUrl, uncommittedChanges, lastCommit };
1389
+ }
1390
+ catch {
1391
+ return null;
1392
+ }
1393
+ }
1394
+ scanEnvFiles(dir) {
1395
+ const findings = [];
1396
+ const envFiles = ['.env', '.env.local', '.env.development', '.env.production', '.env.example'];
1397
+ for (const envFile of envFiles) {
1398
+ const filePath = join(dir, envFile);
1399
+ if (!existsSync(filePath))
1400
+ continue;
1401
+ try {
1402
+ const content = readFileSync(filePath, 'utf-8');
1403
+ const lines = content.split('\n');
1404
+ const variables = [];
1405
+ let hasSecrets = false;
1406
+ for (const line of lines) {
1407
+ if (line.startsWith('#') || !line.includes('='))
1408
+ continue;
1409
+ const [key] = line.split('=');
1410
+ if (key) {
1411
+ variables.push(key.trim());
1412
+ // Check if variable name suggests a secret
1413
+ const keyLower = key.toLowerCase();
1414
+ if (keyLower.includes('secret') || keyLower.includes('password') ||
1415
+ keyLower.includes('key') || keyLower.includes('token') ||
1416
+ keyLower.includes('credential')) {
1417
+ hasSecrets = true;
1418
+ }
1419
+ }
1420
+ }
1421
+ findings.push({
1422
+ file: envFile,
1423
+ variables,
1424
+ hasSecrets
1425
+ });
1426
+ }
1427
+ catch {
1428
+ // Ignore
1429
+ }
1430
+ }
1431
+ return findings;
1432
+ }
1433
+ // Convert scan result to AuditorInput for visualization
1434
+ toAuditorInput(result) {
1435
+ const filesChanged = [
1436
+ ...result.secrets.map(s => s.file),
1437
+ ...result.envFiles.map(e => e.file),
1438
+ ...result.sastFindings.map(s => s.file),
1439
+ ...result.iacFindings.map(i => i.file),
1440
+ ...result.dockerfileFindings.map(d => d.file)
1441
+ ];
1442
+ // Build diff from all findings
1443
+ const diffParts = [];
1444
+ // Secrets
1445
+ for (const s of result.secrets) {
1446
+ diffParts.push(`+[SECRET:${s.type}] ${s.file}:${s.line} - ${s.snippet}`);
1447
+ }
1448
+ // Package vulnerabilities
1449
+ for (const p of result.packages) {
1450
+ diffParts.push(`+[VULN:${p.severity.toUpperCase()}] ${p.name}@${p.version} - ${p.title || p.vulnId || 'vulnerability found'}`);
1451
+ }
1452
+ // SAST findings
1453
+ for (const s of result.sastFindings) {
1454
+ diffParts.push(`+[SAST:${s.severity}] ${s.file}:${s.line} - ${s.rule}: ${s.message}`);
1455
+ }
1456
+ // IaC findings
1457
+ for (const i of result.iacFindings) {
1458
+ diffParts.push(`+[IAC:${i.severity.toUpperCase()}] ${i.file} - ${i.checkId}: ${i.title}`);
1459
+ }
1460
+ // Dockerfile findings
1461
+ for (const d of result.dockerfileFindings) {
1462
+ diffParts.push(`+[DOCKERFILE:${d.severity.toUpperCase()}] ${d.file}:${d.line} - ${d.code}: ${d.message}`);
1463
+ }
1464
+ const changeEvent = {
1465
+ id: `local-scan-${Date.now()}`,
1466
+ type: 'pull_request',
1467
+ environment: 'dev',
1468
+ repo: result.path,
1469
+ commit: result.gitInfo?.lastCommit?.split(' ')[0] || 'local',
1470
+ files_changed: [...new Set(filesChanged)],
1471
+ diff: diffParts.join('\n')
1472
+ };
1473
+ // Count SAST severity (normalize from semgrep format)
1474
+ const normalizeSastSeverity = (sev) => {
1475
+ const s = sev?.toLowerCase() || '';
1476
+ if (s === 'error' || s === 'critical')
1477
+ return 'critical';
1478
+ if (s === 'warning' || s === 'high')
1479
+ return 'high';
1480
+ if (s === 'info' || s === 'medium')
1481
+ return 'medium';
1482
+ return 'low';
1483
+ };
1484
+ const vulnSummary = {
1485
+ critical: result.secrets.filter(s => s.severity === 'critical').length +
1486
+ result.packages.filter(p => p.severity === 'critical').length +
1487
+ result.sastFindings.filter(s => normalizeSastSeverity(s.severity) === 'critical').length +
1488
+ result.iacFindings.filter(i => i.severity === 'critical').length +
1489
+ result.dockerfileFindings.filter(d => d.severity === 'critical').length,
1490
+ high: result.secrets.filter(s => s.severity === 'high').length +
1491
+ result.packages.filter(p => p.severity === 'high').length +
1492
+ result.sastFindings.filter(s => normalizeSastSeverity(s.severity) === 'high').length +
1493
+ result.iacFindings.filter(i => i.severity === 'high').length +
1494
+ result.dockerfileFindings.filter(d => d.severity === 'high').length,
1495
+ medium: result.secrets.filter(s => s.severity === 'medium').length +
1496
+ result.packages.filter(p => p.severity === 'medium').length +
1497
+ result.sastFindings.filter(s => normalizeSastSeverity(s.severity) === 'medium').length +
1498
+ result.iacFindings.filter(i => i.severity === 'medium').length +
1499
+ result.dockerfileFindings.filter(d => d.severity === 'medium').length,
1500
+ low: result.secrets.filter(s => s.severity === 'low').length +
1501
+ result.packages.filter(p => p.severity === 'low').length +
1502
+ result.sastFindings.filter(s => normalizeSastSeverity(s.severity) === 'low').length +
1503
+ result.iacFindings.filter(i => i.severity === 'low').length +
1504
+ result.dockerfileFindings.filter(d => d.severity === 'low').length
1505
+ };
1506
+ const evidenceBundle = {
1507
+ vuln_scan: `critical: ${vulnSummary.critical}\nhigh: ${vulnSummary.high}\nmedium: ${vulnSummary.medium}\nlow: ${vulnSummary.low}`,
1508
+ sbom: result.packages.length > 0
1509
+ ? `Packages scanned: ${result.packages.length}\nVulnerable: ${result.packages.filter(p => p.vulnerabilities > 0).length}`
1510
+ : undefined,
1511
+ sast_results: result.sastFindings.length > 0
1512
+ ? `SAST findings: ${result.sastFindings.length}\nTools: ${result.toolsUsed.join(', ')}`
1513
+ : undefined,
1514
+ iac_scan: result.iacFindings.length > 0
1515
+ ? `IaC findings: ${result.iacFindings.length}\nFrameworks: ${[...new Set(result.iacFindings.map(i => i.checkType))].join(', ')}`
1516
+ : undefined
1517
+ };
1518
+ const criticalAssets = [];
1519
+ if (result.secrets.some(s => s.type.toLowerCase().includes('aws')))
1520
+ criticalAssets.push('infra');
1521
+ if (result.secrets.some(s => s.type.toLowerCase().includes('password')))
1522
+ criticalAssets.push('auth');
1523
+ if (result.secrets.some(s => s.type.toLowerCase().includes('database') || s.type.toLowerCase().includes('connection')))
1524
+ criticalAssets.push('database');
1525
+ if (result.envFiles.some(e => e.hasSecrets))
1526
+ criticalAssets.push('secrets');
1527
+ if (result.sastFindings.some(s => s.rule.toLowerCase().includes('sql') || s.rule.toLowerCase().includes('injection')))
1528
+ criticalAssets.push('database');
1529
+ if (result.sastFindings.some(s => s.rule.toLowerCase().includes('xss') || s.rule.toLowerCase().includes('csrf')))
1530
+ criticalAssets.push('api');
1531
+ if (result.iacFindings.length > 0)
1532
+ criticalAssets.push('infra');
1533
+ if (result.dockerfileFindings.length > 0)
1534
+ criticalAssets.push('infra');
1535
+ return {
1536
+ change_event: changeEvent,
1537
+ evidence_bundle: evidenceBundle,
1538
+ policy_context: {
1539
+ critical_assets: criticalAssets.length > 0 ? criticalAssets : ['secrets'],
1540
+ risk_tolerance: 'low'
1541
+ }
1542
+ };
1543
+ }
1544
+ }
1545
+ // Quick scan function for CLI/API use
1546
+ export async function quickLocalScan(targetPath) {
1547
+ const scanner = new LocalScanner({ targetPath });
1548
+ return scanner.scan();
1549
+ }
1550
+ // Clone a git repo and scan it
1551
+ export async function scanRemoteGit(config) {
1552
+ const { gitUrl, branch = 'main', depth = 1 } = config;
1553
+ console.log(`[SCANNER] Cloning remote repo: ${gitUrl}`);
1554
+ // Create temp directory
1555
+ const tempDir = mkdtempSync(join(tmpdir(), 'aura-scan-'));
1556
+ const cloneStart = Date.now();
1557
+ try {
1558
+ // Clone the repository
1559
+ const cloneArgs = ['clone', '--depth', String(depth)];
1560
+ if (branch) {
1561
+ cloneArgs.push('--branch', branch);
1562
+ }
1563
+ cloneArgs.push(gitUrl, tempDir);
1564
+ const cloneResult = spawnSync('git', cloneArgs, {
1565
+ encoding: 'utf-8',
1566
+ timeout: 120000, // 2 minute timeout for clone
1567
+ maxBuffer: 50 * 1024 * 1024
1568
+ });
1569
+ if (cloneResult.status !== 0) {
1570
+ // Try without branch specification (maybe main vs master)
1571
+ const retryArgs = ['clone', '--depth', String(depth), gitUrl, tempDir + '-retry'];
1572
+ const retryResult = spawnSync('git', retryArgs, {
1573
+ encoding: 'utf-8',
1574
+ timeout: 120000,
1575
+ maxBuffer: 50 * 1024 * 1024
1576
+ });
1577
+ if (retryResult.status !== 0) {
1578
+ throw new Error(`Git clone failed: ${cloneResult.stderr || retryResult.stderr}`);
1579
+ }
1580
+ // Use retry directory
1581
+ rmSync(tempDir, { recursive: true, force: true });
1582
+ const retryDir = tempDir + '-retry';
1583
+ const cloneDuration = Date.now() - cloneStart;
1584
+ console.log(`[SCANNER] Clone completed in ${cloneDuration}ms`);
1585
+ // Run the scan
1586
+ const scanStart = Date.now();
1587
+ const scanner = new LocalScanner({
1588
+ targetPath: retryDir,
1589
+ scanSecrets: config.scanSecrets ?? true,
1590
+ scanPackages: config.scanPackages ?? true
1591
+ });
1592
+ const scanResult = await scanner.scan();
1593
+ const scanDuration = Date.now() - scanStart;
1594
+ // Cleanup
1595
+ try {
1596
+ rmSync(retryDir, { recursive: true, force: true });
1597
+ }
1598
+ catch (e) {
1599
+ console.warn('[SCANNER] Failed to cleanup temp dir:', e);
1600
+ }
1601
+ return {
1602
+ ...scanResult,
1603
+ gitUrl,
1604
+ branch: 'default',
1605
+ cloneDuration,
1606
+ scanDuration
1607
+ };
1608
+ }
1609
+ const cloneDuration = Date.now() - cloneStart;
1610
+ console.log(`[SCANNER] Clone completed in ${cloneDuration}ms`);
1611
+ // Run the scan
1612
+ const scanStart = Date.now();
1613
+ const scanner = new LocalScanner({
1614
+ targetPath: tempDir,
1615
+ scanSecrets: config.scanSecrets ?? true,
1616
+ scanPackages: config.scanPackages ?? true
1617
+ });
1618
+ const scanResult = await scanner.scan();
1619
+ const scanDuration = Date.now() - scanStart;
1620
+ console.log(`[SCANNER] Scan completed in ${scanDuration}ms`);
1621
+ // Cleanup temp directory
1622
+ try {
1623
+ rmSync(tempDir, { recursive: true, force: true });
1624
+ console.log('[SCANNER] Cleaned up temp directory');
1625
+ }
1626
+ catch (e) {
1627
+ console.warn('[SCANNER] Failed to cleanup temp dir:', e);
1628
+ }
1629
+ return {
1630
+ ...scanResult,
1631
+ gitUrl,
1632
+ branch,
1633
+ cloneDuration,
1634
+ scanDuration
1635
+ };
1636
+ }
1637
+ catch (error) {
1638
+ // Cleanup on error
1639
+ try {
1640
+ rmSync(tempDir, { recursive: true, force: true });
1641
+ }
1642
+ catch { }
1643
+ throw error;
1644
+ }
1645
+ }
1646
+ // Check if a string is a git URL
1647
+ export function isGitUrl(str) {
1648
+ return str.startsWith('git@') ||
1649
+ str.startsWith('https://github.com') ||
1650
+ str.startsWith('https://gitlab.com') ||
1651
+ str.startsWith('https://bitbucket.org') ||
1652
+ str.endsWith('.git') ||
1653
+ /^https?:\/\/.*\/([\w-]+)\/([\w-]+)(\.git)?$/.test(str);
1654
+ }