@cencori/scan 0.1.1 → 0.3.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/dist/cli.js +512 -37
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +512 -37
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +228 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +228 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
package/dist/cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
|
+
import { confirm } from "@inquirer/prompts";
|
|
7
8
|
|
|
8
9
|
// src/scanner/index.ts
|
|
9
10
|
import * as fs from "fs";
|
|
@@ -65,6 +66,12 @@ var SECRET_PATTERNS = [
|
|
|
65
66
|
pattern: /sk_test_[0-9a-zA-Z]{24,}/g,
|
|
66
67
|
severity: "medium"
|
|
67
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "Stripe Webhook Secret",
|
|
71
|
+
provider: "Stripe",
|
|
72
|
+
pattern: /whsec_[a-zA-Z0-9]{24,}/g,
|
|
73
|
+
severity: "critical"
|
|
74
|
+
},
|
|
68
75
|
// AWS
|
|
69
76
|
{
|
|
70
77
|
name: "AWS Access Key ID",
|
|
@@ -91,6 +98,12 @@ var SECRET_PATTERNS = [
|
|
|
91
98
|
pattern: /gho_[a-zA-Z0-9]{36}/g,
|
|
92
99
|
severity: "critical"
|
|
93
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: "GitHub Webhook Secret",
|
|
103
|
+
provider: "GitHub",
|
|
104
|
+
pattern: /sha256=[a-fA-F0-9]{64}/g,
|
|
105
|
+
severity: "high"
|
|
106
|
+
},
|
|
94
107
|
// Telegram
|
|
95
108
|
{
|
|
96
109
|
name: "Telegram Bot Token",
|
|
@@ -173,13 +186,56 @@ var SECRET_PATTERNS = [
|
|
|
173
186
|
pattern: /hf_[a-zA-Z0-9]{34}/g,
|
|
174
187
|
severity: "critical"
|
|
175
188
|
},
|
|
176
|
-
//
|
|
189
|
+
// JWT Secrets
|
|
177
190
|
{
|
|
178
|
-
name: "
|
|
179
|
-
provider: "
|
|
180
|
-
pattern: /[
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
name: "JWT Secret Assignment",
|
|
192
|
+
provider: "Generic",
|
|
193
|
+
pattern: /JWT_SECRET\s*[:=]\s*["'][^"']{16,}["']/gi,
|
|
194
|
+
severity: "critical"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "Hardcoded JWT Sign",
|
|
198
|
+
provider: "Generic",
|
|
199
|
+
pattern: /jwt\.(sign|verify)\s*\([^,]+,\s*["'][^"']{10,}["']/gi,
|
|
200
|
+
severity: "critical"
|
|
201
|
+
},
|
|
202
|
+
// OAuth Secrets
|
|
203
|
+
{
|
|
204
|
+
name: "OAuth Client Secret",
|
|
205
|
+
provider: "Generic",
|
|
206
|
+
pattern: /client_secret\s*[:=]\s*["'][a-zA-Z0-9_-]{20,}["']/gi,
|
|
207
|
+
severity: "critical"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "Google Client Secret",
|
|
211
|
+
provider: "Google",
|
|
212
|
+
pattern: /GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']+["']/gi,
|
|
213
|
+
severity: "critical"
|
|
214
|
+
},
|
|
215
|
+
// Database Connection Strings
|
|
216
|
+
{
|
|
217
|
+
name: "MongoDB Connection String",
|
|
218
|
+
provider: "MongoDB",
|
|
219
|
+
pattern: /mongodb(\+srv)?:\/\/[^@\s]+@[^\s"']+/g,
|
|
220
|
+
severity: "critical"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "PostgreSQL Connection String",
|
|
224
|
+
provider: "PostgreSQL",
|
|
225
|
+
pattern: /postgres(ql)?:\/\/[^\s"']+/g,
|
|
226
|
+
severity: "critical"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "MySQL Connection String",
|
|
230
|
+
provider: "MySQL",
|
|
231
|
+
pattern: /mysql:\/\/[^\s"']+/g,
|
|
232
|
+
severity: "critical"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "Redis Connection String",
|
|
236
|
+
provider: "Redis",
|
|
237
|
+
pattern: /redis:\/\/[^\s"']+/g,
|
|
238
|
+
severity: "high"
|
|
183
239
|
}
|
|
184
240
|
];
|
|
185
241
|
var PII_PATTERNS = [
|
|
@@ -215,7 +271,7 @@ var PII_PATTERNS = [
|
|
|
215
271
|
}
|
|
216
272
|
];
|
|
217
273
|
var ROUTE_PATTERNS = [
|
|
218
|
-
// Next.js API routes
|
|
274
|
+
// Next.js API routes
|
|
219
275
|
{
|
|
220
276
|
name: "Next.js API Route (check for auth)",
|
|
221
277
|
framework: "Next.js",
|
|
@@ -230,6 +286,120 @@ var ROUTE_PATTERNS = [
|
|
|
230
286
|
pattern: /app\.(get|post|put|delete|patch)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?!.*auth)/gi,
|
|
231
287
|
severity: "medium",
|
|
232
288
|
description: "Express route - check if auth middleware is applied"
|
|
289
|
+
},
|
|
290
|
+
// Admin routes
|
|
291
|
+
{
|
|
292
|
+
name: "Admin Route Exposed",
|
|
293
|
+
framework: "Generic",
|
|
294
|
+
pattern: /["'`](\/admin|\/dashboard|\/internal|\/private)[^"'`]*["'`]/gi,
|
|
295
|
+
severity: "high",
|
|
296
|
+
description: "Sensitive route - ensure proper authentication"
|
|
297
|
+
}
|
|
298
|
+
];
|
|
299
|
+
var VULNERABILITY_PATTERNS = [
|
|
300
|
+
// Hardcoded URLs
|
|
301
|
+
{
|
|
302
|
+
name: "Localhost URL in Code",
|
|
303
|
+
category: "hardcoded-url",
|
|
304
|
+
pattern: /https?:\/\/localhost[:\d]*/gi,
|
|
305
|
+
severity: "medium",
|
|
306
|
+
description: "Development URL - should use environment variables"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "Staging/Dev URL in Code",
|
|
310
|
+
category: "hardcoded-url",
|
|
311
|
+
pattern: /https?:\/\/(staging\.|dev\.|test\.)[^\s"']+/gi,
|
|
312
|
+
severity: "medium",
|
|
313
|
+
description: "Non-production URL in code"
|
|
314
|
+
},
|
|
315
|
+
// Debug artifacts (skip console.log - too many false positives for CLI tools)
|
|
316
|
+
{
|
|
317
|
+
name: "Debug Flag Enabled",
|
|
318
|
+
category: "debug",
|
|
319
|
+
pattern: /DEBUG\s*[:=]\s*(true|1|["']true["'])/gi,
|
|
320
|
+
severity: "medium",
|
|
321
|
+
description: "Debug mode enabled - disable in production"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "Hardcoded Development Mode",
|
|
325
|
+
category: "debug",
|
|
326
|
+
pattern: /NODE_ENV\s*[:=]\s*["']development["']/gi,
|
|
327
|
+
severity: "medium",
|
|
328
|
+
description: "Hardcoded development mode"
|
|
329
|
+
},
|
|
330
|
+
// CORS issues
|
|
331
|
+
{
|
|
332
|
+
name: "CORS Wildcard Origin",
|
|
333
|
+
category: "cors",
|
|
334
|
+
pattern: /Access-Control-Allow-Origin['":\s]+\*/g,
|
|
335
|
+
severity: "high",
|
|
336
|
+
description: "Allows requests from any origin - security risk"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "Permissive CORS Config",
|
|
340
|
+
category: "cors",
|
|
341
|
+
pattern: /cors\s*\(\s*\)/g,
|
|
342
|
+
severity: "medium",
|
|
343
|
+
description: "CORS with default (permissive) settings"
|
|
344
|
+
},
|
|
345
|
+
// SQL Injection
|
|
346
|
+
{
|
|
347
|
+
name: "SQL String Concatenation",
|
|
348
|
+
category: "injection",
|
|
349
|
+
pattern: /query\s*\(\s*[`'"].*\$\{.*\}/g,
|
|
350
|
+
severity: "critical",
|
|
351
|
+
description: "Potential SQL injection - use parameterized queries"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "SQL String Addition",
|
|
355
|
+
category: "injection",
|
|
356
|
+
pattern: /(SELECT|INSERT|UPDATE|DELETE).*["']\s*\+\s*\w+/gi,
|
|
357
|
+
severity: "critical",
|
|
358
|
+
description: "SQL built with string concatenation"
|
|
359
|
+
},
|
|
360
|
+
// XSS Vulnerabilities
|
|
361
|
+
{
|
|
362
|
+
name: "React dangerouslySetInnerHTML",
|
|
363
|
+
category: "xss",
|
|
364
|
+
pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html/g,
|
|
365
|
+
severity: "high",
|
|
366
|
+
description: "Renders raw HTML - ensure input is sanitized"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "Direct innerHTML Assignment",
|
|
370
|
+
category: "xss",
|
|
371
|
+
pattern: /\.innerHTML\s*=/g,
|
|
372
|
+
severity: "high",
|
|
373
|
+
description: "Direct HTML injection - use textContent instead"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "Vue v-html Directive",
|
|
377
|
+
category: "xss",
|
|
378
|
+
pattern: /v-html\s*=\s*["'][^"']+["']/g,
|
|
379
|
+
severity: "high",
|
|
380
|
+
description: "Vue raw HTML binding - ensure input is sanitized"
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "Document Write",
|
|
384
|
+
category: "xss",
|
|
385
|
+
pattern: /document\.write\s*\(/g,
|
|
386
|
+
severity: "high",
|
|
387
|
+
description: "Deprecated and potentially dangerous"
|
|
388
|
+
},
|
|
389
|
+
// Eval and code execution
|
|
390
|
+
{
|
|
391
|
+
name: "Eval Usage",
|
|
392
|
+
category: "injection",
|
|
393
|
+
pattern: /\beval\s*\(/g,
|
|
394
|
+
severity: "critical",
|
|
395
|
+
description: "Code execution - major security risk"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "Function Constructor",
|
|
399
|
+
category: "injection",
|
|
400
|
+
pattern: /new\s+Function\s*\(/g,
|
|
401
|
+
severity: "high",
|
|
402
|
+
description: "Dynamic code execution risk"
|
|
233
403
|
}
|
|
234
404
|
];
|
|
235
405
|
var IGNORE_PATTERNS = [
|
|
@@ -270,7 +440,9 @@ var SCANNABLE_EXTENSIONS = [
|
|
|
270
440
|
".sql",
|
|
271
441
|
".sh",
|
|
272
442
|
".bash",
|
|
273
|
-
".zsh"
|
|
443
|
+
".zsh",
|
|
444
|
+
".vue",
|
|
445
|
+
".svelte"
|
|
274
446
|
];
|
|
275
447
|
|
|
276
448
|
// src/scanner/index.ts
|
|
@@ -300,9 +472,14 @@ function isScannable(filePath) {
|
|
|
300
472
|
const ext = path.extname(filePath).toLowerCase();
|
|
301
473
|
return SCANNABLE_EXTENSIONS.includes(ext);
|
|
302
474
|
}
|
|
475
|
+
function isDocOrTestFile(filePath) {
|
|
476
|
+
const lower = filePath.toLowerCase();
|
|
477
|
+
return lower.includes(".test.") || lower.includes(".spec.") || lower.includes("__tests__") || lower.includes("/test/") || lower.includes("/tests/") || lower.endsWith(".md") || lower.includes("/docs/");
|
|
478
|
+
}
|
|
303
479
|
function scanFile(filePath, content) {
|
|
304
480
|
const issues = [];
|
|
305
481
|
const relativePath = filePath;
|
|
482
|
+
const isDocFile = isDocOrTestFile(filePath);
|
|
306
483
|
for (const pattern of SECRET_PATTERNS) {
|
|
307
484
|
pattern.pattern.lastIndex = 0;
|
|
308
485
|
let match;
|
|
@@ -320,47 +497,73 @@ function scanFile(filePath, content) {
|
|
|
320
497
|
});
|
|
321
498
|
}
|
|
322
499
|
}
|
|
323
|
-
|
|
500
|
+
if (!isDocFile) {
|
|
501
|
+
for (const pattern of PII_PATTERNS) {
|
|
502
|
+
pattern.pattern.lastIndex = 0;
|
|
503
|
+
let match;
|
|
504
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
505
|
+
const matchStr = match[0];
|
|
506
|
+
if (isLikelyFalsePositive(matchStr, pattern.name, filePath)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const pos = getPosition(content, match.index);
|
|
510
|
+
issues.push({
|
|
511
|
+
type: "pii",
|
|
512
|
+
severity: pattern.severity,
|
|
513
|
+
name: pattern.name,
|
|
514
|
+
file: relativePath,
|
|
515
|
+
line: pos.line,
|
|
516
|
+
column: pos.column,
|
|
517
|
+
match: redact(matchStr, 3)
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const pattern of ROUTE_PATTERNS) {
|
|
324
523
|
pattern.pattern.lastIndex = 0;
|
|
325
524
|
let match;
|
|
326
525
|
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
327
|
-
const matchStr = match[0];
|
|
328
|
-
if (isLikelyFalsePositive(matchStr, pattern.name)) {
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
526
|
const pos = getPosition(content, match.index);
|
|
332
527
|
issues.push({
|
|
333
|
-
type: "
|
|
528
|
+
type: "route",
|
|
334
529
|
severity: pattern.severity,
|
|
335
530
|
name: pattern.name,
|
|
336
531
|
file: relativePath,
|
|
337
532
|
line: pos.line,
|
|
338
533
|
column: pos.column,
|
|
339
|
-
match:
|
|
534
|
+
match: match[0],
|
|
535
|
+
description: pattern.description
|
|
340
536
|
});
|
|
341
537
|
}
|
|
342
538
|
}
|
|
343
|
-
for (const pattern of
|
|
539
|
+
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
540
|
+
if (pattern.category === "debug" && isDocFile) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
344
543
|
pattern.pattern.lastIndex = 0;
|
|
345
544
|
let match;
|
|
346
545
|
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
546
|
+
if (pattern.category === "debug" && pattern.name === "Console Log Statement") {
|
|
547
|
+
if (match[0].includes("error") || match[0].includes("warn")) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
347
551
|
const pos = getPosition(content, match.index);
|
|
348
552
|
issues.push({
|
|
349
|
-
type: "
|
|
553
|
+
type: "vulnerability",
|
|
554
|
+
category: pattern.category,
|
|
350
555
|
severity: pattern.severity,
|
|
351
556
|
name: pattern.name,
|
|
352
557
|
file: relativePath,
|
|
353
558
|
line: pos.line,
|
|
354
559
|
column: pos.column,
|
|
355
|
-
match: match[0],
|
|
560
|
+
match: match[0].length > 50 ? match[0].slice(0, 50) + "..." : match[0],
|
|
356
561
|
description: pattern.description
|
|
357
562
|
});
|
|
358
563
|
}
|
|
359
564
|
}
|
|
360
565
|
const fileName = path.basename(filePath);
|
|
361
566
|
if (fileName.startsWith(".env") && !fileName.includes(".example")) {
|
|
362
|
-
const gitignorePath = path.join(path.dirname(filePath), ".gitignore");
|
|
363
|
-
const gitignoreExists = fs.existsSync(gitignorePath);
|
|
364
567
|
issues.push({
|
|
365
568
|
type: "config",
|
|
366
569
|
severity: "high",
|
|
@@ -369,20 +572,14 @@ function scanFile(filePath, content) {
|
|
|
369
572
|
line: 1,
|
|
370
573
|
column: 1,
|
|
371
574
|
match: fileName,
|
|
372
|
-
description:
|
|
575
|
+
description: "Add .env* to .gitignore"
|
|
373
576
|
});
|
|
374
577
|
}
|
|
375
578
|
return issues;
|
|
376
579
|
}
|
|
377
|
-
function isLikelyFalsePositive(match, patternName) {
|
|
580
|
+
function isLikelyFalsePositive(match, patternName, filePath) {
|
|
378
581
|
if (patternName === "Email Address") {
|
|
379
|
-
const falseDomains = [
|
|
380
|
-
"example.com",
|
|
381
|
-
"example.org",
|
|
382
|
-
"test.com",
|
|
383
|
-
"localhost",
|
|
384
|
-
"placeholder.com"
|
|
385
|
-
];
|
|
582
|
+
const falseDomains = ["example.com", "example.org", "test.com", "localhost", "placeholder.com"];
|
|
386
583
|
if (falseDomains.some((d) => match.includes(d))) {
|
|
387
584
|
return true;
|
|
388
585
|
}
|
|
@@ -424,13 +621,12 @@ function calculateScore(issues) {
|
|
|
424
621
|
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
425
622
|
const high = issues.filter((i) => i.severity === "high").length;
|
|
426
623
|
const medium = issues.filter((i) => i.severity === "medium").length;
|
|
427
|
-
const total = issues.length;
|
|
428
624
|
if (critical > 0) return "F";
|
|
429
625
|
if (high >= 3) return "F";
|
|
430
626
|
if (high >= 2) return "D";
|
|
431
627
|
if (high >= 1 || medium >= 5) return "C";
|
|
432
628
|
if (medium >= 2) return "B";
|
|
433
|
-
if (
|
|
629
|
+
if (issues.length === 0) return "A";
|
|
434
630
|
return "B";
|
|
435
631
|
}
|
|
436
632
|
function getTierDescription(score) {
|
|
@@ -444,7 +640,7 @@ function getTierDescription(score) {
|
|
|
444
640
|
case "D":
|
|
445
641
|
return "Poor. Significant security issues detected.";
|
|
446
642
|
case "F":
|
|
447
|
-
return "Critical!
|
|
643
|
+
return "Critical! Major security vulnerabilities found.";
|
|
448
644
|
default:
|
|
449
645
|
return "";
|
|
450
646
|
}
|
|
@@ -487,6 +683,7 @@ async function scan(targetPath) {
|
|
|
487
683
|
pii: issues.filter((i) => i.type === "pii").length,
|
|
488
684
|
routes: issues.filter((i) => i.type === "route").length,
|
|
489
685
|
config: issues.filter((i) => i.type === "config").length,
|
|
686
|
+
vulnerabilities: issues.filter((i) => i.type === "vulnerability").length,
|
|
490
687
|
critical: issues.filter((i) => i.severity === "critical").length,
|
|
491
688
|
high: issues.filter((i) => i.severity === "high").length,
|
|
492
689
|
medium: issues.filter((i) => i.severity === "medium").length,
|
|
@@ -495,8 +692,177 @@ async function scan(targetPath) {
|
|
|
495
692
|
};
|
|
496
693
|
}
|
|
497
694
|
|
|
695
|
+
// src/ai/index.ts
|
|
696
|
+
var CENCORI_API_URL = "https://api.cencori.com/v1";
|
|
697
|
+
function getApiKey() {
|
|
698
|
+
return process.env.CENCORI_API_KEY;
|
|
699
|
+
}
|
|
700
|
+
function isAIAvailable() {
|
|
701
|
+
return !!getApiKey();
|
|
702
|
+
}
|
|
703
|
+
async function analyzeIssues(issues, fileContents) {
|
|
704
|
+
const apiKey = getApiKey();
|
|
705
|
+
if (!apiKey) {
|
|
706
|
+
throw new Error("CENCORI_API_KEY not set");
|
|
707
|
+
}
|
|
708
|
+
const results = [];
|
|
709
|
+
for (const issue of issues) {
|
|
710
|
+
const content = fileContents.get(issue.file) || "";
|
|
711
|
+
const lines = content.split("\n");
|
|
712
|
+
const startLine = Math.max(0, issue.line - 3);
|
|
713
|
+
const endLine = Math.min(lines.length, issue.line + 3);
|
|
714
|
+
const context = lines.slice(startLine, endLine).join("\n");
|
|
715
|
+
try {
|
|
716
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
717
|
+
method: "POST",
|
|
718
|
+
headers: {
|
|
719
|
+
"Content-Type": "application/json",
|
|
720
|
+
"Authorization": `Bearer ${apiKey}`
|
|
721
|
+
},
|
|
722
|
+
body: JSON.stringify({
|
|
723
|
+
model: "gpt-4o-mini",
|
|
724
|
+
messages: [
|
|
725
|
+
{
|
|
726
|
+
role: "system",
|
|
727
|
+
content: `You are a security analyst. Analyze code findings and determine if they are real security issues or false positives. Respond in JSON format: {"isFalsePositive": boolean, "confidence": number (0-100), "reason": "brief explanation"}`
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
role: "user",
|
|
731
|
+
content: `Analyze this security finding:
|
|
732
|
+
Type: ${issue.type}
|
|
733
|
+
Name: ${issue.name}
|
|
734
|
+
Match: ${issue.match}
|
|
735
|
+
File: ${issue.file}:${issue.line}
|
|
736
|
+
Context:
|
|
737
|
+
\`\`\`
|
|
738
|
+
${context}
|
|
739
|
+
\`\`\`
|
|
740
|
+
|
|
741
|
+
Is this a real security issue or a false positive (e.g., test data, example code, documentation)?`
|
|
742
|
+
}
|
|
743
|
+
],
|
|
744
|
+
temperature: 0,
|
|
745
|
+
max_tokens: 150
|
|
746
|
+
})
|
|
747
|
+
});
|
|
748
|
+
if (!response.ok) {
|
|
749
|
+
throw new Error(`API error: ${response.status}`);
|
|
750
|
+
}
|
|
751
|
+
const data = await response.json();
|
|
752
|
+
const content_response = data.choices[0]?.message?.content || "{}";
|
|
753
|
+
const parsed = JSON.parse(content_response);
|
|
754
|
+
results.push({
|
|
755
|
+
issue,
|
|
756
|
+
isFalsePositive: parsed.isFalsePositive || false,
|
|
757
|
+
confidence: parsed.confidence || 50,
|
|
758
|
+
reason: parsed.reason || "Unable to analyze"
|
|
759
|
+
});
|
|
760
|
+
} catch {
|
|
761
|
+
results.push({
|
|
762
|
+
issue,
|
|
763
|
+
isFalsePositive: false,
|
|
764
|
+
confidence: 50,
|
|
765
|
+
reason: "Analysis failed - treating as potential issue"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return results;
|
|
770
|
+
}
|
|
771
|
+
async function generateFixes(issues, fileContents) {
|
|
772
|
+
const apiKey = getApiKey();
|
|
773
|
+
if (!apiKey) {
|
|
774
|
+
throw new Error("CENCORI_API_KEY not set");
|
|
775
|
+
}
|
|
776
|
+
const results = [];
|
|
777
|
+
for (const issue of issues) {
|
|
778
|
+
const content = fileContents.get(issue.file) || "";
|
|
779
|
+
const lines = content.split("\n");
|
|
780
|
+
const startLine = Math.max(0, issue.line - 5);
|
|
781
|
+
const endLine = Math.min(lines.length, issue.line + 5);
|
|
782
|
+
const codeSnippet = lines.slice(startLine, endLine).join("\n");
|
|
783
|
+
try {
|
|
784
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
headers: {
|
|
787
|
+
"Content-Type": "application/json",
|
|
788
|
+
"Authorization": `Bearer ${apiKey}`
|
|
789
|
+
},
|
|
790
|
+
body: JSON.stringify({
|
|
791
|
+
model: "gpt-4o-mini",
|
|
792
|
+
messages: [
|
|
793
|
+
{
|
|
794
|
+
role: "system",
|
|
795
|
+
content: `You are a security engineer. Generate secure code fixes. For secrets, use environment variables. For XSS, use sanitization. Respond in JSON: {"fixedCode": "the fixed code snippet", "explanation": "what was changed"}`
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
role: "user",
|
|
799
|
+
content: `Fix this security issue:
|
|
800
|
+
Type: ${issue.type}
|
|
801
|
+
Name: ${issue.name}
|
|
802
|
+
File: ${issue.file}:${issue.line}
|
|
803
|
+
|
|
804
|
+
Code to fix:
|
|
805
|
+
\`\`\`
|
|
806
|
+
${codeSnippet}
|
|
807
|
+
\`\`\`
|
|
808
|
+
|
|
809
|
+
Generate a secure fix.`
|
|
810
|
+
}
|
|
811
|
+
],
|
|
812
|
+
temperature: 0,
|
|
813
|
+
max_tokens: 500
|
|
814
|
+
})
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
throw new Error(`API error: ${response.status}`);
|
|
818
|
+
}
|
|
819
|
+
const data = await response.json();
|
|
820
|
+
const content_response = data.choices[0]?.message?.content || "{}";
|
|
821
|
+
const parsed = JSON.parse(content_response);
|
|
822
|
+
results.push({
|
|
823
|
+
issue,
|
|
824
|
+
originalCode: codeSnippet,
|
|
825
|
+
fixedCode: parsed.fixedCode || codeSnippet,
|
|
826
|
+
explanation: parsed.explanation || "No explanation provided",
|
|
827
|
+
applied: false
|
|
828
|
+
});
|
|
829
|
+
} catch {
|
|
830
|
+
results.push({
|
|
831
|
+
issue,
|
|
832
|
+
originalCode: codeSnippet,
|
|
833
|
+
fixedCode: codeSnippet,
|
|
834
|
+
explanation: "Unable to generate fix - manual review required",
|
|
835
|
+
applied: false
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return results;
|
|
840
|
+
}
|
|
841
|
+
async function applyFixes(fixes, fileContents) {
|
|
842
|
+
const fs3 = await import("fs");
|
|
843
|
+
const path3 = await import("path");
|
|
844
|
+
for (const fix of fixes) {
|
|
845
|
+
if (fix.fixedCode === fix.originalCode) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const content = fileContents.get(fix.issue.file);
|
|
849
|
+
if (!content) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const newContent = content.replace(fix.originalCode, fix.fixedCode);
|
|
853
|
+
if (newContent !== content) {
|
|
854
|
+
const filePath = path3.resolve(fix.issue.file);
|
|
855
|
+
fs3.writeFileSync(filePath, newContent, "utf-8");
|
|
856
|
+
fix.applied = true;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return fixes;
|
|
860
|
+
}
|
|
861
|
+
|
|
498
862
|
// src/cli.ts
|
|
499
|
-
|
|
863
|
+
import * as fs2 from "fs";
|
|
864
|
+
import * as path2 from "path";
|
|
865
|
+
var VERSION = "0.3.0";
|
|
500
866
|
var scoreStyles = {
|
|
501
867
|
A: { color: chalk.green },
|
|
502
868
|
B: { color: chalk.blue },
|
|
@@ -514,7 +880,8 @@ var typeLabels = {
|
|
|
514
880
|
secret: "SECRETS",
|
|
515
881
|
pii: "PII",
|
|
516
882
|
route: "ROUTES",
|
|
517
|
-
config: "CONFIG"
|
|
883
|
+
config: "CONFIG",
|
|
884
|
+
vulnerability: "VULNERABILITIES"
|
|
518
885
|
};
|
|
519
886
|
function printBanner() {
|
|
520
887
|
console.log();
|
|
@@ -588,12 +955,15 @@ function printSummary(result) {
|
|
|
588
955
|
}
|
|
589
956
|
console.log();
|
|
590
957
|
}
|
|
591
|
-
function
|
|
958
|
+
function printRecommendations(issues) {
|
|
592
959
|
if (issues.length === 0) return;
|
|
593
960
|
console.log(` ${chalk.bold("Recommendations:")}`);
|
|
594
961
|
const hasSecrets = issues.some((i) => i.type === "secret");
|
|
595
962
|
const hasPII = issues.some((i) => i.type === "pii");
|
|
596
963
|
const hasConfig = issues.some((i) => i.type === "config");
|
|
964
|
+
const hasXSS = issues.some((i) => i.category === "xss");
|
|
965
|
+
const hasInjection = issues.some((i) => i.category === "injection");
|
|
966
|
+
const hasCORS = issues.some((i) => i.category === "cors");
|
|
597
967
|
if (hasSecrets) {
|
|
598
968
|
console.log(chalk.gray(" - Use environment variables for secrets"));
|
|
599
969
|
console.log(chalk.gray(" - Never commit API keys to version control"));
|
|
@@ -604,6 +974,26 @@ function printFixes(issues) {
|
|
|
604
974
|
if (hasPII) {
|
|
605
975
|
console.log(chalk.gray(" - Remove personal data from source code"));
|
|
606
976
|
}
|
|
977
|
+
if (hasXSS) {
|
|
978
|
+
console.log(chalk.gray(" - Sanitize user input before rendering HTML"));
|
|
979
|
+
}
|
|
980
|
+
if (hasInjection) {
|
|
981
|
+
console.log(chalk.gray(" - Use parameterized queries for SQL"));
|
|
982
|
+
}
|
|
983
|
+
if (hasCORS) {
|
|
984
|
+
console.log(chalk.gray(" - Configure CORS with specific allowed origins"));
|
|
985
|
+
}
|
|
986
|
+
console.log();
|
|
987
|
+
}
|
|
988
|
+
function printAIUpsell() {
|
|
989
|
+
console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
990
|
+
console.log();
|
|
991
|
+
console.log(` ${chalk.cyan.bold("Upgrade to Cencori Pro")}`);
|
|
992
|
+
console.log(chalk.gray(" Get AI-powered auto-fix for all issues:"));
|
|
993
|
+
console.log();
|
|
994
|
+
console.log(` 1. Get your API key at ${chalk.cyan("https://cencori.com/dashboard")}`);
|
|
995
|
+
console.log(` 2. Run: ${chalk.cyan("export CENCORI_API_KEY=your_key")}`);
|
|
996
|
+
console.log(` 3. Scan again to unlock AI auto-fix`);
|
|
607
997
|
console.log();
|
|
608
998
|
}
|
|
609
999
|
function printFooter() {
|
|
@@ -613,8 +1003,90 @@ function printFooter() {
|
|
|
613
1003
|
console.log(` Docs: ${chalk.cyan("https://cencori.com/docs")}`);
|
|
614
1004
|
console.log();
|
|
615
1005
|
}
|
|
1006
|
+
function loadFileContents(issues, basePath) {
|
|
1007
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1008
|
+
const uniqueFiles = [...new Set(issues.map((i) => i.file))];
|
|
1009
|
+
for (const file of uniqueFiles) {
|
|
1010
|
+
try {
|
|
1011
|
+
const fullPath = path2.resolve(basePath, file);
|
|
1012
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
1013
|
+
contents.set(file, content);
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return contents;
|
|
1018
|
+
}
|
|
1019
|
+
async function handleAutoFix(result, targetPath) {
|
|
1020
|
+
if (result.issues.length === 0) return;
|
|
1021
|
+
console.log();
|
|
1022
|
+
if (!isAIAvailable()) {
|
|
1023
|
+
printAIUpsell();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const shouldFix = await confirm({
|
|
1027
|
+
message: "Would you like Cencori to auto-fix these issues?",
|
|
1028
|
+
default: false
|
|
1029
|
+
});
|
|
1030
|
+
if (!shouldFix) {
|
|
1031
|
+
console.log();
|
|
1032
|
+
console.log(chalk.gray(" Skipped auto-fix. Run again anytime to fix issues."));
|
|
1033
|
+
console.log();
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const fileContents = loadFileContents(result.issues, targetPath);
|
|
1037
|
+
const analyzeSpinner = ora({
|
|
1038
|
+
text: "Analyzing issues with AI...",
|
|
1039
|
+
color: "cyan"
|
|
1040
|
+
}).start();
|
|
1041
|
+
try {
|
|
1042
|
+
const analysis = await analyzeIssues(result.issues, fileContents);
|
|
1043
|
+
const realIssues = analysis.filter((a) => !a.isFalsePositive);
|
|
1044
|
+
const falsePositives = analysis.filter((a) => a.isFalsePositive);
|
|
1045
|
+
if (falsePositives.length > 0) {
|
|
1046
|
+
analyzeSpinner.succeed(`${chalk.green(falsePositives.length)} false positives filtered`);
|
|
1047
|
+
} else {
|
|
1048
|
+
analyzeSpinner.succeed("Analysis complete");
|
|
1049
|
+
}
|
|
1050
|
+
if (realIssues.length === 0) {
|
|
1051
|
+
console.log(chalk.green(" All issues were false positives!"));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const fixSpinner = ora({
|
|
1055
|
+
text: "Generating fixes...",
|
|
1056
|
+
color: "cyan"
|
|
1057
|
+
}).start();
|
|
1058
|
+
const fixes = await generateFixes(
|
|
1059
|
+
realIssues.map((a) => a.issue),
|
|
1060
|
+
fileContents
|
|
1061
|
+
);
|
|
1062
|
+
fixSpinner.succeed(`Generated ${fixes.length} fixes`);
|
|
1063
|
+
const applySpinner = ora({
|
|
1064
|
+
text: "Applying fixes...",
|
|
1065
|
+
color: "cyan"
|
|
1066
|
+
}).start();
|
|
1067
|
+
const appliedFixes = await applyFixes(fixes, fileContents);
|
|
1068
|
+
const appliedCount = appliedFixes.filter((f) => f.applied).length;
|
|
1069
|
+
applySpinner.succeed(`Applied ${appliedCount}/${fixes.length} fixes`);
|
|
1070
|
+
console.log();
|
|
1071
|
+
console.log(` ${chalk.bold("Applied fixes:")}`);
|
|
1072
|
+
for (const fix of appliedFixes.filter((f) => f.applied)) {
|
|
1073
|
+
console.log(chalk.green(` \u2714 ${fix.issue.file}:${fix.issue.line}`));
|
|
1074
|
+
console.log(chalk.gray(` ${fix.explanation}`));
|
|
1075
|
+
}
|
|
1076
|
+
const notApplied = appliedFixes.filter((f) => !f.applied);
|
|
1077
|
+
if (notApplied.length > 0) {
|
|
1078
|
+
console.log();
|
|
1079
|
+
console.log(` ${chalk.yellow(`${notApplied.length} issues require manual review`)}`);
|
|
1080
|
+
}
|
|
1081
|
+
console.log();
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
analyzeSpinner.fail("Auto-fix failed");
|
|
1084
|
+
console.error(chalk.red(` Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
1085
|
+
console.log();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
616
1088
|
async function main() {
|
|
617
|
-
program.name("cencori-scan").description("Security scanner for AI apps. Detect secrets, PII, and exposed routes.").version(VERSION).argument("[path]", "Path to scan", ".").option("-j, --json", "Output results as JSON").option("-q, --quiet", "Only output the score").option("--no-color", "Disable colored output").action(async (targetPath, options) => {
|
|
1089
|
+
program.name("cencori-scan").description("Security scanner for AI apps. Detect secrets, PII, and exposed routes.").version(VERSION).argument("[path]", "Path to scan", ".").option("-j, --json", "Output results as JSON").option("-q, --quiet", "Only output the score").option("--no-prompt", "Skip interactive prompts").option("--no-color", "Disable colored output").action(async (targetPath, options) => {
|
|
618
1090
|
if (options.json) {
|
|
619
1091
|
const result = await scan(targetPath);
|
|
620
1092
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -640,7 +1112,10 @@ async function main() {
|
|
|
640
1112
|
printScore(result);
|
|
641
1113
|
printIssues(result.issues);
|
|
642
1114
|
printSummary(result);
|
|
643
|
-
|
|
1115
|
+
printRecommendations(result.issues);
|
|
1116
|
+
if (options.prompt !== false && result.issues.length > 0) {
|
|
1117
|
+
await handleAutoFix(result, targetPath);
|
|
1118
|
+
}
|
|
644
1119
|
printFooter();
|
|
645
1120
|
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
646
1121
|
} catch (error) {
|