@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 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
- // Cohere
212
+ // JWT Secrets
200
213
  {
201
- name: "Cohere API Key",
202
- provider: "Cohere",
203
- pattern: /[a-zA-Z0-9]{40}/g,
204
- // Less specific, check context
205
- severity: "medium"
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 without auth
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
- for (const pattern of PII_PATTERNS) {
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: "pii",
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: redact(matchStr, 3)
557
+ match: match[0],
558
+ description: pattern.description
363
559
  });
364
560
  }
365
561
  }
366
- for (const pattern of ROUTE_PATTERNS) {
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: "route",
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: gitignoreExists ? "Verify this file is in .gitignore" : "Add .env* to .gitignore"
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 (total === 0) return "A";
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! Your app is leaking secrets.";
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 VERSION = "0.1.0";
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 printFixes(issues) {
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
- printFixes(result.issues);
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) {