@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.js
CHANGED
|
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
var import_chalk = __toESM(require("chalk"));
|
|
29
29
|
var import_ora = __toESM(require("ora"));
|
|
30
|
+
var import_prompts = require("@inquirer/prompts");
|
|
30
31
|
|
|
31
32
|
// src/scanner/index.ts
|
|
32
33
|
var fs = __toESM(require("fs"));
|
|
@@ -88,6 +89,12 @@ var SECRET_PATTERNS = [
|
|
|
88
89
|
pattern: /sk_test_[0-9a-zA-Z]{24,}/g,
|
|
89
90
|
severity: "medium"
|
|
90
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: "Stripe Webhook Secret",
|
|
94
|
+
provider: "Stripe",
|
|
95
|
+
pattern: /whsec_[a-zA-Z0-9]{24,}/g,
|
|
96
|
+
severity: "critical"
|
|
97
|
+
},
|
|
91
98
|
// AWS
|
|
92
99
|
{
|
|
93
100
|
name: "AWS Access Key ID",
|
|
@@ -114,6 +121,12 @@ var SECRET_PATTERNS = [
|
|
|
114
121
|
pattern: /gho_[a-zA-Z0-9]{36}/g,
|
|
115
122
|
severity: "critical"
|
|
116
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: "GitHub Webhook Secret",
|
|
126
|
+
provider: "GitHub",
|
|
127
|
+
pattern: /sha256=[a-fA-F0-9]{64}/g,
|
|
128
|
+
severity: "high"
|
|
129
|
+
},
|
|
117
130
|
// Telegram
|
|
118
131
|
{
|
|
119
132
|
name: "Telegram Bot Token",
|
|
@@ -196,13 +209,56 @@ var SECRET_PATTERNS = [
|
|
|
196
209
|
pattern: /hf_[a-zA-Z0-9]{34}/g,
|
|
197
210
|
severity: "critical"
|
|
198
211
|
},
|
|
199
|
-
//
|
|
212
|
+
// JWT Secrets
|
|
200
213
|
{
|
|
201
|
-
name: "
|
|
202
|
-
provider: "
|
|
203
|
-
pattern: /[
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
name: "JWT Secret Assignment",
|
|
215
|
+
provider: "Generic",
|
|
216
|
+
pattern: /JWT_SECRET\s*[:=]\s*["'][^"']{16,}["']/gi,
|
|
217
|
+
severity: "critical"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "Hardcoded JWT Sign",
|
|
221
|
+
provider: "Generic",
|
|
222
|
+
pattern: /jwt\.(sign|verify)\s*\([^,]+,\s*["'][^"']{10,}["']/gi,
|
|
223
|
+
severity: "critical"
|
|
224
|
+
},
|
|
225
|
+
// OAuth Secrets
|
|
226
|
+
{
|
|
227
|
+
name: "OAuth Client Secret",
|
|
228
|
+
provider: "Generic",
|
|
229
|
+
pattern: /client_secret\s*[:=]\s*["'][a-zA-Z0-9_-]{20,}["']/gi,
|
|
230
|
+
severity: "critical"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "Google Client Secret",
|
|
234
|
+
provider: "Google",
|
|
235
|
+
pattern: /GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']+["']/gi,
|
|
236
|
+
severity: "critical"
|
|
237
|
+
},
|
|
238
|
+
// Database Connection Strings
|
|
239
|
+
{
|
|
240
|
+
name: "MongoDB Connection String",
|
|
241
|
+
provider: "MongoDB",
|
|
242
|
+
pattern: /mongodb(\+srv)?:\/\/[^@\s]+@[^\s"']+/g,
|
|
243
|
+
severity: "critical"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "PostgreSQL Connection String",
|
|
247
|
+
provider: "PostgreSQL",
|
|
248
|
+
pattern: /postgres(ql)?:\/\/[^\s"']+/g,
|
|
249
|
+
severity: "critical"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "MySQL Connection String",
|
|
253
|
+
provider: "MySQL",
|
|
254
|
+
pattern: /mysql:\/\/[^\s"']+/g,
|
|
255
|
+
severity: "critical"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "Redis Connection String",
|
|
259
|
+
provider: "Redis",
|
|
260
|
+
pattern: /redis:\/\/[^\s"']+/g,
|
|
261
|
+
severity: "high"
|
|
206
262
|
}
|
|
207
263
|
];
|
|
208
264
|
var PII_PATTERNS = [
|
|
@@ -238,7 +294,7 @@ var PII_PATTERNS = [
|
|
|
238
294
|
}
|
|
239
295
|
];
|
|
240
296
|
var ROUTE_PATTERNS = [
|
|
241
|
-
// Next.js API routes
|
|
297
|
+
// Next.js API routes
|
|
242
298
|
{
|
|
243
299
|
name: "Next.js API Route (check for auth)",
|
|
244
300
|
framework: "Next.js",
|
|
@@ -253,6 +309,120 @@ var ROUTE_PATTERNS = [
|
|
|
253
309
|
pattern: /app\.(get|post|put|delete|patch)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?!.*auth)/gi,
|
|
254
310
|
severity: "medium",
|
|
255
311
|
description: "Express route - check if auth middleware is applied"
|
|
312
|
+
},
|
|
313
|
+
// Admin routes
|
|
314
|
+
{
|
|
315
|
+
name: "Admin Route Exposed",
|
|
316
|
+
framework: "Generic",
|
|
317
|
+
pattern: /["'`](\/admin|\/dashboard|\/internal|\/private)[^"'`]*["'`]/gi,
|
|
318
|
+
severity: "high",
|
|
319
|
+
description: "Sensitive route - ensure proper authentication"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
var VULNERABILITY_PATTERNS = [
|
|
323
|
+
// Hardcoded URLs
|
|
324
|
+
{
|
|
325
|
+
name: "Localhost URL in Code",
|
|
326
|
+
category: "hardcoded-url",
|
|
327
|
+
pattern: /https?:\/\/localhost[:\d]*/gi,
|
|
328
|
+
severity: "medium",
|
|
329
|
+
description: "Development URL - should use environment variables"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "Staging/Dev URL in Code",
|
|
333
|
+
category: "hardcoded-url",
|
|
334
|
+
pattern: /https?:\/\/(staging\.|dev\.|test\.)[^\s"']+/gi,
|
|
335
|
+
severity: "medium",
|
|
336
|
+
description: "Non-production URL in code"
|
|
337
|
+
},
|
|
338
|
+
// Debug artifacts (skip console.log - too many false positives for CLI tools)
|
|
339
|
+
{
|
|
340
|
+
name: "Debug Flag Enabled",
|
|
341
|
+
category: "debug",
|
|
342
|
+
pattern: /DEBUG\s*[:=]\s*(true|1|["']true["'])/gi,
|
|
343
|
+
severity: "medium",
|
|
344
|
+
description: "Debug mode enabled - disable in production"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "Hardcoded Development Mode",
|
|
348
|
+
category: "debug",
|
|
349
|
+
pattern: /NODE_ENV\s*[:=]\s*["']development["']/gi,
|
|
350
|
+
severity: "medium",
|
|
351
|
+
description: "Hardcoded development mode"
|
|
352
|
+
},
|
|
353
|
+
// CORS issues
|
|
354
|
+
{
|
|
355
|
+
name: "CORS Wildcard Origin",
|
|
356
|
+
category: "cors",
|
|
357
|
+
pattern: /Access-Control-Allow-Origin['":\s]+\*/g,
|
|
358
|
+
severity: "high",
|
|
359
|
+
description: "Allows requests from any origin - security risk"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "Permissive CORS Config",
|
|
363
|
+
category: "cors",
|
|
364
|
+
pattern: /cors\s*\(\s*\)/g,
|
|
365
|
+
severity: "medium",
|
|
366
|
+
description: "CORS with default (permissive) settings"
|
|
367
|
+
},
|
|
368
|
+
// SQL Injection
|
|
369
|
+
{
|
|
370
|
+
name: "SQL String Concatenation",
|
|
371
|
+
category: "injection",
|
|
372
|
+
pattern: /query\s*\(\s*[`'"].*\$\{.*\}/g,
|
|
373
|
+
severity: "critical",
|
|
374
|
+
description: "Potential SQL injection - use parameterized queries"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "SQL String Addition",
|
|
378
|
+
category: "injection",
|
|
379
|
+
pattern: /(SELECT|INSERT|UPDATE|DELETE).*["']\s*\+\s*\w+/gi,
|
|
380
|
+
severity: "critical",
|
|
381
|
+
description: "SQL built with string concatenation"
|
|
382
|
+
},
|
|
383
|
+
// XSS Vulnerabilities
|
|
384
|
+
{
|
|
385
|
+
name: "React dangerouslySetInnerHTML",
|
|
386
|
+
category: "xss",
|
|
387
|
+
pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html/g,
|
|
388
|
+
severity: "high",
|
|
389
|
+
description: "Renders raw HTML - ensure input is sanitized"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "Direct innerHTML Assignment",
|
|
393
|
+
category: "xss",
|
|
394
|
+
pattern: /\.innerHTML\s*=/g,
|
|
395
|
+
severity: "high",
|
|
396
|
+
description: "Direct HTML injection - use textContent instead"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "Vue v-html Directive",
|
|
400
|
+
category: "xss",
|
|
401
|
+
pattern: /v-html\s*=\s*["'][^"']+["']/g,
|
|
402
|
+
severity: "high",
|
|
403
|
+
description: "Vue raw HTML binding - ensure input is sanitized"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "Document Write",
|
|
407
|
+
category: "xss",
|
|
408
|
+
pattern: /document\.write\s*\(/g,
|
|
409
|
+
severity: "high",
|
|
410
|
+
description: "Deprecated and potentially dangerous"
|
|
411
|
+
},
|
|
412
|
+
// Eval and code execution
|
|
413
|
+
{
|
|
414
|
+
name: "Eval Usage",
|
|
415
|
+
category: "injection",
|
|
416
|
+
pattern: /\beval\s*\(/g,
|
|
417
|
+
severity: "critical",
|
|
418
|
+
description: "Code execution - major security risk"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "Function Constructor",
|
|
422
|
+
category: "injection",
|
|
423
|
+
pattern: /new\s+Function\s*\(/g,
|
|
424
|
+
severity: "high",
|
|
425
|
+
description: "Dynamic code execution risk"
|
|
256
426
|
}
|
|
257
427
|
];
|
|
258
428
|
var IGNORE_PATTERNS = [
|
|
@@ -293,7 +463,9 @@ var SCANNABLE_EXTENSIONS = [
|
|
|
293
463
|
".sql",
|
|
294
464
|
".sh",
|
|
295
465
|
".bash",
|
|
296
|
-
".zsh"
|
|
466
|
+
".zsh",
|
|
467
|
+
".vue",
|
|
468
|
+
".svelte"
|
|
297
469
|
];
|
|
298
470
|
|
|
299
471
|
// src/scanner/index.ts
|
|
@@ -323,9 +495,14 @@ function isScannable(filePath) {
|
|
|
323
495
|
const ext = path.extname(filePath).toLowerCase();
|
|
324
496
|
return SCANNABLE_EXTENSIONS.includes(ext);
|
|
325
497
|
}
|
|
498
|
+
function isDocOrTestFile(filePath) {
|
|
499
|
+
const lower = filePath.toLowerCase();
|
|
500
|
+
return lower.includes(".test.") || lower.includes(".spec.") || lower.includes("__tests__") || lower.includes("/test/") || lower.includes("/tests/") || lower.endsWith(".md") || lower.includes("/docs/");
|
|
501
|
+
}
|
|
326
502
|
function scanFile(filePath, content) {
|
|
327
503
|
const issues = [];
|
|
328
504
|
const relativePath = filePath;
|
|
505
|
+
const isDocFile = isDocOrTestFile(filePath);
|
|
329
506
|
for (const pattern of SECRET_PATTERNS) {
|
|
330
507
|
pattern.pattern.lastIndex = 0;
|
|
331
508
|
let match;
|
|
@@ -343,47 +520,73 @@ function scanFile(filePath, content) {
|
|
|
343
520
|
});
|
|
344
521
|
}
|
|
345
522
|
}
|
|
346
|
-
|
|
523
|
+
if (!isDocFile) {
|
|
524
|
+
for (const pattern of PII_PATTERNS) {
|
|
525
|
+
pattern.pattern.lastIndex = 0;
|
|
526
|
+
let match;
|
|
527
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
528
|
+
const matchStr = match[0];
|
|
529
|
+
if (isLikelyFalsePositive(matchStr, pattern.name, filePath)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const pos = getPosition(content, match.index);
|
|
533
|
+
issues.push({
|
|
534
|
+
type: "pii",
|
|
535
|
+
severity: pattern.severity,
|
|
536
|
+
name: pattern.name,
|
|
537
|
+
file: relativePath,
|
|
538
|
+
line: pos.line,
|
|
539
|
+
column: pos.column,
|
|
540
|
+
match: redact(matchStr, 3)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const pattern of ROUTE_PATTERNS) {
|
|
347
546
|
pattern.pattern.lastIndex = 0;
|
|
348
547
|
let match;
|
|
349
548
|
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
350
|
-
const matchStr = match[0];
|
|
351
|
-
if (isLikelyFalsePositive(matchStr, pattern.name)) {
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
549
|
const pos = getPosition(content, match.index);
|
|
355
550
|
issues.push({
|
|
356
|
-
type: "
|
|
551
|
+
type: "route",
|
|
357
552
|
severity: pattern.severity,
|
|
358
553
|
name: pattern.name,
|
|
359
554
|
file: relativePath,
|
|
360
555
|
line: pos.line,
|
|
361
556
|
column: pos.column,
|
|
362
|
-
match:
|
|
557
|
+
match: match[0],
|
|
558
|
+
description: pattern.description
|
|
363
559
|
});
|
|
364
560
|
}
|
|
365
561
|
}
|
|
366
|
-
for (const pattern of
|
|
562
|
+
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
563
|
+
if (pattern.category === "debug" && isDocFile) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
367
566
|
pattern.pattern.lastIndex = 0;
|
|
368
567
|
let match;
|
|
369
568
|
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
569
|
+
if (pattern.category === "debug" && pattern.name === "Console Log Statement") {
|
|
570
|
+
if (match[0].includes("error") || match[0].includes("warn")) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
370
574
|
const pos = getPosition(content, match.index);
|
|
371
575
|
issues.push({
|
|
372
|
-
type: "
|
|
576
|
+
type: "vulnerability",
|
|
577
|
+
category: pattern.category,
|
|
373
578
|
severity: pattern.severity,
|
|
374
579
|
name: pattern.name,
|
|
375
580
|
file: relativePath,
|
|
376
581
|
line: pos.line,
|
|
377
582
|
column: pos.column,
|
|
378
|
-
match: match[0],
|
|
583
|
+
match: match[0].length > 50 ? match[0].slice(0, 50) + "..." : match[0],
|
|
379
584
|
description: pattern.description
|
|
380
585
|
});
|
|
381
586
|
}
|
|
382
587
|
}
|
|
383
588
|
const fileName = path.basename(filePath);
|
|
384
589
|
if (fileName.startsWith(".env") && !fileName.includes(".example")) {
|
|
385
|
-
const gitignorePath = path.join(path.dirname(filePath), ".gitignore");
|
|
386
|
-
const gitignoreExists = fs.existsSync(gitignorePath);
|
|
387
590
|
issues.push({
|
|
388
591
|
type: "config",
|
|
389
592
|
severity: "high",
|
|
@@ -392,20 +595,14 @@ function scanFile(filePath, content) {
|
|
|
392
595
|
line: 1,
|
|
393
596
|
column: 1,
|
|
394
597
|
match: fileName,
|
|
395
|
-
description:
|
|
598
|
+
description: "Add .env* to .gitignore"
|
|
396
599
|
});
|
|
397
600
|
}
|
|
398
601
|
return issues;
|
|
399
602
|
}
|
|
400
|
-
function isLikelyFalsePositive(match, patternName) {
|
|
603
|
+
function isLikelyFalsePositive(match, patternName, filePath) {
|
|
401
604
|
if (patternName === "Email Address") {
|
|
402
|
-
const falseDomains = [
|
|
403
|
-
"example.com",
|
|
404
|
-
"example.org",
|
|
405
|
-
"test.com",
|
|
406
|
-
"localhost",
|
|
407
|
-
"placeholder.com"
|
|
408
|
-
];
|
|
605
|
+
const falseDomains = ["example.com", "example.org", "test.com", "localhost", "placeholder.com"];
|
|
409
606
|
if (falseDomains.some((d) => match.includes(d))) {
|
|
410
607
|
return true;
|
|
411
608
|
}
|
|
@@ -447,13 +644,12 @@ function calculateScore(issues) {
|
|
|
447
644
|
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
448
645
|
const high = issues.filter((i) => i.severity === "high").length;
|
|
449
646
|
const medium = issues.filter((i) => i.severity === "medium").length;
|
|
450
|
-
const total = issues.length;
|
|
451
647
|
if (critical > 0) return "F";
|
|
452
648
|
if (high >= 3) return "F";
|
|
453
649
|
if (high >= 2) return "D";
|
|
454
650
|
if (high >= 1 || medium >= 5) return "C";
|
|
455
651
|
if (medium >= 2) return "B";
|
|
456
|
-
if (
|
|
652
|
+
if (issues.length === 0) return "A";
|
|
457
653
|
return "B";
|
|
458
654
|
}
|
|
459
655
|
function getTierDescription(score) {
|
|
@@ -467,7 +663,7 @@ function getTierDescription(score) {
|
|
|
467
663
|
case "D":
|
|
468
664
|
return "Poor. Significant security issues detected.";
|
|
469
665
|
case "F":
|
|
470
|
-
return "Critical!
|
|
666
|
+
return "Critical! Major security vulnerabilities found.";
|
|
471
667
|
default:
|
|
472
668
|
return "";
|
|
473
669
|
}
|
|
@@ -510,6 +706,7 @@ async function scan(targetPath) {
|
|
|
510
706
|
pii: issues.filter((i) => i.type === "pii").length,
|
|
511
707
|
routes: issues.filter((i) => i.type === "route").length,
|
|
512
708
|
config: issues.filter((i) => i.type === "config").length,
|
|
709
|
+
vulnerabilities: issues.filter((i) => i.type === "vulnerability").length,
|
|
513
710
|
critical: issues.filter((i) => i.severity === "critical").length,
|
|
514
711
|
high: issues.filter((i) => i.severity === "high").length,
|
|
515
712
|
medium: issues.filter((i) => i.severity === "medium").length,
|
|
@@ -518,8 +715,177 @@ async function scan(targetPath) {
|
|
|
518
715
|
};
|
|
519
716
|
}
|
|
520
717
|
|
|
718
|
+
// src/ai/index.ts
|
|
719
|
+
var CENCORI_API_URL = "https://api.cencori.com/v1";
|
|
720
|
+
function getApiKey() {
|
|
721
|
+
return process.env.CENCORI_API_KEY;
|
|
722
|
+
}
|
|
723
|
+
function isAIAvailable() {
|
|
724
|
+
return !!getApiKey();
|
|
725
|
+
}
|
|
726
|
+
async function analyzeIssues(issues, fileContents) {
|
|
727
|
+
const apiKey = getApiKey();
|
|
728
|
+
if (!apiKey) {
|
|
729
|
+
throw new Error("CENCORI_API_KEY not set");
|
|
730
|
+
}
|
|
731
|
+
const results = [];
|
|
732
|
+
for (const issue of issues) {
|
|
733
|
+
const content = fileContents.get(issue.file) || "";
|
|
734
|
+
const lines = content.split("\n");
|
|
735
|
+
const startLine = Math.max(0, issue.line - 3);
|
|
736
|
+
const endLine = Math.min(lines.length, issue.line + 3);
|
|
737
|
+
const context = lines.slice(startLine, endLine).join("\n");
|
|
738
|
+
try {
|
|
739
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: {
|
|
742
|
+
"Content-Type": "application/json",
|
|
743
|
+
"Authorization": `Bearer ${apiKey}`
|
|
744
|
+
},
|
|
745
|
+
body: JSON.stringify({
|
|
746
|
+
model: "gpt-4o-mini",
|
|
747
|
+
messages: [
|
|
748
|
+
{
|
|
749
|
+
role: "system",
|
|
750
|
+
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"}`
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
role: "user",
|
|
754
|
+
content: `Analyze this security finding:
|
|
755
|
+
Type: ${issue.type}
|
|
756
|
+
Name: ${issue.name}
|
|
757
|
+
Match: ${issue.match}
|
|
758
|
+
File: ${issue.file}:${issue.line}
|
|
759
|
+
Context:
|
|
760
|
+
\`\`\`
|
|
761
|
+
${context}
|
|
762
|
+
\`\`\`
|
|
763
|
+
|
|
764
|
+
Is this a real security issue or a false positive (e.g., test data, example code, documentation)?`
|
|
765
|
+
}
|
|
766
|
+
],
|
|
767
|
+
temperature: 0,
|
|
768
|
+
max_tokens: 150
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
throw new Error(`API error: ${response.status}`);
|
|
773
|
+
}
|
|
774
|
+
const data = await response.json();
|
|
775
|
+
const content_response = data.choices[0]?.message?.content || "{}";
|
|
776
|
+
const parsed = JSON.parse(content_response);
|
|
777
|
+
results.push({
|
|
778
|
+
issue,
|
|
779
|
+
isFalsePositive: parsed.isFalsePositive || false,
|
|
780
|
+
confidence: parsed.confidence || 50,
|
|
781
|
+
reason: parsed.reason || "Unable to analyze"
|
|
782
|
+
});
|
|
783
|
+
} catch {
|
|
784
|
+
results.push({
|
|
785
|
+
issue,
|
|
786
|
+
isFalsePositive: false,
|
|
787
|
+
confidence: 50,
|
|
788
|
+
reason: "Analysis failed - treating as potential issue"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return results;
|
|
793
|
+
}
|
|
794
|
+
async function generateFixes(issues, fileContents) {
|
|
795
|
+
const apiKey = getApiKey();
|
|
796
|
+
if (!apiKey) {
|
|
797
|
+
throw new Error("CENCORI_API_KEY not set");
|
|
798
|
+
}
|
|
799
|
+
const results = [];
|
|
800
|
+
for (const issue of issues) {
|
|
801
|
+
const content = fileContents.get(issue.file) || "";
|
|
802
|
+
const lines = content.split("\n");
|
|
803
|
+
const startLine = Math.max(0, issue.line - 5);
|
|
804
|
+
const endLine = Math.min(lines.length, issue.line + 5);
|
|
805
|
+
const codeSnippet = lines.slice(startLine, endLine).join("\n");
|
|
806
|
+
try {
|
|
807
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
808
|
+
method: "POST",
|
|
809
|
+
headers: {
|
|
810
|
+
"Content-Type": "application/json",
|
|
811
|
+
"Authorization": `Bearer ${apiKey}`
|
|
812
|
+
},
|
|
813
|
+
body: JSON.stringify({
|
|
814
|
+
model: "gpt-4o-mini",
|
|
815
|
+
messages: [
|
|
816
|
+
{
|
|
817
|
+
role: "system",
|
|
818
|
+
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"}`
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
role: "user",
|
|
822
|
+
content: `Fix this security issue:
|
|
823
|
+
Type: ${issue.type}
|
|
824
|
+
Name: ${issue.name}
|
|
825
|
+
File: ${issue.file}:${issue.line}
|
|
826
|
+
|
|
827
|
+
Code to fix:
|
|
828
|
+
\`\`\`
|
|
829
|
+
${codeSnippet}
|
|
830
|
+
\`\`\`
|
|
831
|
+
|
|
832
|
+
Generate a secure fix.`
|
|
833
|
+
}
|
|
834
|
+
],
|
|
835
|
+
temperature: 0,
|
|
836
|
+
max_tokens: 500
|
|
837
|
+
})
|
|
838
|
+
});
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
throw new Error(`API error: ${response.status}`);
|
|
841
|
+
}
|
|
842
|
+
const data = await response.json();
|
|
843
|
+
const content_response = data.choices[0]?.message?.content || "{}";
|
|
844
|
+
const parsed = JSON.parse(content_response);
|
|
845
|
+
results.push({
|
|
846
|
+
issue,
|
|
847
|
+
originalCode: codeSnippet,
|
|
848
|
+
fixedCode: parsed.fixedCode || codeSnippet,
|
|
849
|
+
explanation: parsed.explanation || "No explanation provided",
|
|
850
|
+
applied: false
|
|
851
|
+
});
|
|
852
|
+
} catch {
|
|
853
|
+
results.push({
|
|
854
|
+
issue,
|
|
855
|
+
originalCode: codeSnippet,
|
|
856
|
+
fixedCode: codeSnippet,
|
|
857
|
+
explanation: "Unable to generate fix - manual review required",
|
|
858
|
+
applied: false
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return results;
|
|
863
|
+
}
|
|
864
|
+
async function applyFixes(fixes, fileContents) {
|
|
865
|
+
const fs3 = await import("fs");
|
|
866
|
+
const path3 = await import("path");
|
|
867
|
+
for (const fix of fixes) {
|
|
868
|
+
if (fix.fixedCode === fix.originalCode) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const content = fileContents.get(fix.issue.file);
|
|
872
|
+
if (!content) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const newContent = content.replace(fix.originalCode, fix.fixedCode);
|
|
876
|
+
if (newContent !== content) {
|
|
877
|
+
const filePath = path3.resolve(fix.issue.file);
|
|
878
|
+
fs3.writeFileSync(filePath, newContent, "utf-8");
|
|
879
|
+
fix.applied = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return fixes;
|
|
883
|
+
}
|
|
884
|
+
|
|
521
885
|
// src/cli.ts
|
|
522
|
-
var
|
|
886
|
+
var fs2 = __toESM(require("fs"));
|
|
887
|
+
var path2 = __toESM(require("path"));
|
|
888
|
+
var VERSION = "0.3.0";
|
|
523
889
|
var scoreStyles = {
|
|
524
890
|
A: { color: import_chalk.default.green },
|
|
525
891
|
B: { color: import_chalk.default.blue },
|
|
@@ -537,7 +903,8 @@ var typeLabels = {
|
|
|
537
903
|
secret: "SECRETS",
|
|
538
904
|
pii: "PII",
|
|
539
905
|
route: "ROUTES",
|
|
540
|
-
config: "CONFIG"
|
|
906
|
+
config: "CONFIG",
|
|
907
|
+
vulnerability: "VULNERABILITIES"
|
|
541
908
|
};
|
|
542
909
|
function printBanner() {
|
|
543
910
|
console.log();
|
|
@@ -611,12 +978,15 @@ function printSummary(result) {
|
|
|
611
978
|
}
|
|
612
979
|
console.log();
|
|
613
980
|
}
|
|
614
|
-
function
|
|
981
|
+
function printRecommendations(issues) {
|
|
615
982
|
if (issues.length === 0) return;
|
|
616
983
|
console.log(` ${import_chalk.default.bold("Recommendations:")}`);
|
|
617
984
|
const hasSecrets = issues.some((i) => i.type === "secret");
|
|
618
985
|
const hasPII = issues.some((i) => i.type === "pii");
|
|
619
986
|
const hasConfig = issues.some((i) => i.type === "config");
|
|
987
|
+
const hasXSS = issues.some((i) => i.category === "xss");
|
|
988
|
+
const hasInjection = issues.some((i) => i.category === "injection");
|
|
989
|
+
const hasCORS = issues.some((i) => i.category === "cors");
|
|
620
990
|
if (hasSecrets) {
|
|
621
991
|
console.log(import_chalk.default.gray(" - Use environment variables for secrets"));
|
|
622
992
|
console.log(import_chalk.default.gray(" - Never commit API keys to version control"));
|
|
@@ -627,6 +997,26 @@ function printFixes(issues) {
|
|
|
627
997
|
if (hasPII) {
|
|
628
998
|
console.log(import_chalk.default.gray(" - Remove personal data from source code"));
|
|
629
999
|
}
|
|
1000
|
+
if (hasXSS) {
|
|
1001
|
+
console.log(import_chalk.default.gray(" - Sanitize user input before rendering HTML"));
|
|
1002
|
+
}
|
|
1003
|
+
if (hasInjection) {
|
|
1004
|
+
console.log(import_chalk.default.gray(" - Use parameterized queries for SQL"));
|
|
1005
|
+
}
|
|
1006
|
+
if (hasCORS) {
|
|
1007
|
+
console.log(import_chalk.default.gray(" - Configure CORS with specific allowed origins"));
|
|
1008
|
+
}
|
|
1009
|
+
console.log();
|
|
1010
|
+
}
|
|
1011
|
+
function printAIUpsell() {
|
|
1012
|
+
console.log(import_chalk.default.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"));
|
|
1013
|
+
console.log();
|
|
1014
|
+
console.log(` ${import_chalk.default.cyan.bold("Upgrade to Cencori Pro")}`);
|
|
1015
|
+
console.log(import_chalk.default.gray(" Get AI-powered auto-fix for all issues:"));
|
|
1016
|
+
console.log();
|
|
1017
|
+
console.log(` 1. Get your API key at ${import_chalk.default.cyan("https://cencori.com/dashboard")}`);
|
|
1018
|
+
console.log(` 2. Run: ${import_chalk.default.cyan("export CENCORI_API_KEY=your_key")}`);
|
|
1019
|
+
console.log(` 3. Scan again to unlock AI auto-fix`);
|
|
630
1020
|
console.log();
|
|
631
1021
|
}
|
|
632
1022
|
function printFooter() {
|
|
@@ -636,8 +1026,90 @@ function printFooter() {
|
|
|
636
1026
|
console.log(` Docs: ${import_chalk.default.cyan("https://cencori.com/docs")}`);
|
|
637
1027
|
console.log();
|
|
638
1028
|
}
|
|
1029
|
+
function loadFileContents(issues, basePath) {
|
|
1030
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1031
|
+
const uniqueFiles = [...new Set(issues.map((i) => i.file))];
|
|
1032
|
+
for (const file of uniqueFiles) {
|
|
1033
|
+
try {
|
|
1034
|
+
const fullPath = path2.resolve(basePath, file);
|
|
1035
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
1036
|
+
contents.set(file, content);
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return contents;
|
|
1041
|
+
}
|
|
1042
|
+
async function handleAutoFix(result, targetPath) {
|
|
1043
|
+
if (result.issues.length === 0) return;
|
|
1044
|
+
console.log();
|
|
1045
|
+
if (!isAIAvailable()) {
|
|
1046
|
+
printAIUpsell();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const shouldFix = await (0, import_prompts.confirm)({
|
|
1050
|
+
message: "Would you like Cencori to auto-fix these issues?",
|
|
1051
|
+
default: false
|
|
1052
|
+
});
|
|
1053
|
+
if (!shouldFix) {
|
|
1054
|
+
console.log();
|
|
1055
|
+
console.log(import_chalk.default.gray(" Skipped auto-fix. Run again anytime to fix issues."));
|
|
1056
|
+
console.log();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const fileContents = loadFileContents(result.issues, targetPath);
|
|
1060
|
+
const analyzeSpinner = (0, import_ora.default)({
|
|
1061
|
+
text: "Analyzing issues with AI...",
|
|
1062
|
+
color: "cyan"
|
|
1063
|
+
}).start();
|
|
1064
|
+
try {
|
|
1065
|
+
const analysis = await analyzeIssues(result.issues, fileContents);
|
|
1066
|
+
const realIssues = analysis.filter((a) => !a.isFalsePositive);
|
|
1067
|
+
const falsePositives = analysis.filter((a) => a.isFalsePositive);
|
|
1068
|
+
if (falsePositives.length > 0) {
|
|
1069
|
+
analyzeSpinner.succeed(`${import_chalk.default.green(falsePositives.length)} false positives filtered`);
|
|
1070
|
+
} else {
|
|
1071
|
+
analyzeSpinner.succeed("Analysis complete");
|
|
1072
|
+
}
|
|
1073
|
+
if (realIssues.length === 0) {
|
|
1074
|
+
console.log(import_chalk.default.green(" All issues were false positives!"));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const fixSpinner = (0, import_ora.default)({
|
|
1078
|
+
text: "Generating fixes...",
|
|
1079
|
+
color: "cyan"
|
|
1080
|
+
}).start();
|
|
1081
|
+
const fixes = await generateFixes(
|
|
1082
|
+
realIssues.map((a) => a.issue),
|
|
1083
|
+
fileContents
|
|
1084
|
+
);
|
|
1085
|
+
fixSpinner.succeed(`Generated ${fixes.length} fixes`);
|
|
1086
|
+
const applySpinner = (0, import_ora.default)({
|
|
1087
|
+
text: "Applying fixes...",
|
|
1088
|
+
color: "cyan"
|
|
1089
|
+
}).start();
|
|
1090
|
+
const appliedFixes = await applyFixes(fixes, fileContents);
|
|
1091
|
+
const appliedCount = appliedFixes.filter((f) => f.applied).length;
|
|
1092
|
+
applySpinner.succeed(`Applied ${appliedCount}/${fixes.length} fixes`);
|
|
1093
|
+
console.log();
|
|
1094
|
+
console.log(` ${import_chalk.default.bold("Applied fixes:")}`);
|
|
1095
|
+
for (const fix of appliedFixes.filter((f) => f.applied)) {
|
|
1096
|
+
console.log(import_chalk.default.green(` \u2714 ${fix.issue.file}:${fix.issue.line}`));
|
|
1097
|
+
console.log(import_chalk.default.gray(` ${fix.explanation}`));
|
|
1098
|
+
}
|
|
1099
|
+
const notApplied = appliedFixes.filter((f) => !f.applied);
|
|
1100
|
+
if (notApplied.length > 0) {
|
|
1101
|
+
console.log();
|
|
1102
|
+
console.log(` ${import_chalk.default.yellow(`${notApplied.length} issues require manual review`)}`);
|
|
1103
|
+
}
|
|
1104
|
+
console.log();
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
analyzeSpinner.fail("Auto-fix failed");
|
|
1107
|
+
console.error(import_chalk.default.red(` Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
1108
|
+
console.log();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
639
1111
|
async function main() {
|
|
640
|
-
import_commander.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) => {
|
|
1112
|
+
import_commander.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) => {
|
|
641
1113
|
if (options.json) {
|
|
642
1114
|
const result = await scan(targetPath);
|
|
643
1115
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -663,7 +1135,10 @@ async function main() {
|
|
|
663
1135
|
printScore(result);
|
|
664
1136
|
printIssues(result.issues);
|
|
665
1137
|
printSummary(result);
|
|
666
|
-
|
|
1138
|
+
printRecommendations(result.issues);
|
|
1139
|
+
if (options.prompt !== false && result.issues.length > 0) {
|
|
1140
|
+
await handleAutoFix(result, targetPath);
|
|
1141
|
+
}
|
|
667
1142
|
printFooter();
|
|
668
1143
|
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
669
1144
|
} catch (error) {
|