@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.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
- // Cohere
189
+ // JWT Secrets
177
190
  {
178
- name: "Cohere API Key",
179
- provider: "Cohere",
180
- pattern: /[a-zA-Z0-9]{40}/g,
181
- // Less specific, check context
182
- severity: "medium"
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 without auth
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
- for (const pattern of PII_PATTERNS) {
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: "pii",
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: redact(matchStr, 3)
534
+ match: match[0],
535
+ description: pattern.description
340
536
  });
341
537
  }
342
538
  }
343
- for (const pattern of ROUTE_PATTERNS) {
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: "route",
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: gitignoreExists ? "Verify this file is in .gitignore" : "Add .env* to .gitignore"
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 (total === 0) return "A";
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! Your app is leaking secrets.";
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
- var VERSION = "0.1.0";
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 printFixes(issues) {
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
- printFixes(result.issues);
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) {