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.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/deploy/AWS-DEPLOYMENT.md +358 -0
- package/deploy/terraform/main.tf +362 -0
- package/deploy/terraform/terraform.tfvars.example +6 -0
- package/dist/agents/base.d.ts +44 -0
- package/dist/agents/base.js +96 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.js +17 -0
- package/dist/agents/policy/evaluator.d.ts +15 -0
- package/dist/agents/policy/evaluator.js +183 -0
- package/dist/agents/policy/index.d.ts +12 -0
- package/dist/agents/policy/index.js +15 -0
- package/dist/agents/policy/validator.d.ts +15 -0
- package/dist/agents/policy/validator.js +182 -0
- package/dist/agents/scanners/gitleaks.d.ts +14 -0
- package/dist/agents/scanners/gitleaks.js +155 -0
- package/dist/agents/scanners/grype.d.ts +14 -0
- package/dist/agents/scanners/grype.js +109 -0
- package/dist/agents/scanners/index.d.ts +15 -0
- package/dist/agents/scanners/index.js +27 -0
- package/dist/agents/scanners/npm-audit.d.ts +13 -0
- package/dist/agents/scanners/npm-audit.js +129 -0
- package/dist/agents/scanners/semgrep.d.ts +14 -0
- package/dist/agents/scanners/semgrep.js +131 -0
- package/dist/agents/scanners/trivy.d.ts +14 -0
- package/dist/agents/scanners/trivy.js +122 -0
- package/dist/agents/types.d.ts +137 -0
- package/dist/agents/types.js +91 -0
- package/dist/auditor/index.d.ts +3 -0
- package/dist/auditor/index.js +2 -0
- package/dist/auditor/pipeline.d.ts +19 -0
- package/dist/auditor/pipeline.js +240 -0
- package/dist/auditor/validator.d.ts +17 -0
- package/dist/auditor/validator.js +58 -0
- package/dist/aura/client.d.ts +29 -0
- package/dist/aura/client.js +125 -0
- package/dist/aura/index.d.ts +4 -0
- package/dist/aura/index.js +2 -0
- package/dist/aura/server.d.ts +45 -0
- package/dist/aura/server.js +343 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +1433 -0
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +170 -0
- package/dist/compliance/index.d.ts +40 -0
- package/dist/compliance/index.js +292 -0
- package/dist/database/index.d.ts +77 -0
- package/dist/database/index.js +395 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +762 -0
- package/dist/integrations/aura-scanner.d.ts +69 -0
- package/dist/integrations/aura-scanner.js +155 -0
- package/dist/integrations/aws-scanner.d.ts +63 -0
- package/dist/integrations/aws-scanner.js +624 -0
- package/dist/integrations/config.d.ts +69 -0
- package/dist/integrations/config.js +212 -0
- package/dist/integrations/github.d.ts +45 -0
- package/dist/integrations/github.js +201 -0
- package/dist/integrations/gitlab.d.ts +36 -0
- package/dist/integrations/gitlab.js +110 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/local-scanner.d.ts +146 -0
- package/dist/integrations/local-scanner.js +1654 -0
- package/dist/integrations/notifications.d.ts +99 -0
- package/dist/integrations/notifications.js +305 -0
- package/dist/integrations/scanners.d.ts +57 -0
- package/dist/integrations/scanners.js +217 -0
- package/dist/integrations/slop-scanner.d.ts +69 -0
- package/dist/integrations/slop-scanner.js +155 -0
- package/dist/integrations/webhook.d.ts +37 -0
- package/dist/integrations/webhook.js +256 -0
- package/dist/orchestrator/index.d.ts +72 -0
- package/dist/orchestrator/index.js +187 -0
- package/dist/output/index.d.ts +152 -0
- package/dist/output/index.js +399 -0
- package/dist/pipeline/index.d.ts +72 -0
- package/dist/pipeline/index.js +313 -0
- package/dist/sbom/index.d.ts +94 -0
- package/dist/sbom/index.js +298 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/input.schema.d.ts +87 -0
- package/dist/schemas/input.schema.js +44 -0
- package/dist/schemas/output.schema.d.ts +115 -0
- package/dist/schemas/output.schema.js +64 -0
- package/dist/serve-visualizer.d.ts +2 -0
- package/dist/serve-visualizer.js +78 -0
- package/dist/slop/client.d.ts +29 -0
- package/dist/slop/client.js +125 -0
- package/dist/slop/index.d.ts +4 -0
- package/dist/slop/index.js +2 -0
- package/dist/slop/server.d.ts +45 -0
- package/dist/slop/server.js +343 -0
- package/dist/types/events.d.ts +62 -0
- package/dist/types/events.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/visualizer/index.d.ts +4 -0
- package/dist/visualizer/index.js +181 -0
- package/dist/websocket/index.d.ts +88 -0
- package/dist/websocket/index.js +195 -0
- package/dist/zones/index.d.ts +7 -0
- package/dist/zones/index.js +7 -0
- package/dist/zones/manager.d.ts +101 -0
- package/dist/zones/manager.js +304 -0
- package/dist/zones/types.d.ts +78 -0
- package/dist/zones/types.js +33 -0
- package/package.json +84 -0
- package/visualizer/app.js +0 -0
- package/visualizer/index-minimal.html +1771 -0
- package/visualizer/index.html +2933 -0
- package/visualizer/landing.html +1328 -0
- 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
|
+
}
|