@cencori/scan 0.4.4 → 0.4.6

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,7 +4,7 @@
4
4
  import { program } from "commander";
5
5
  import chalk from "chalk";
6
6
  import ora from "ora";
7
- import { confirm, password } from "@inquirer/prompts";
7
+ import { confirm, password, select } from "@inquirer/prompts";
8
8
 
9
9
  // src/scanner/index.ts
10
10
  import * as fs from "fs";
@@ -833,17 +833,29 @@ async function generateFixes(issues, fileContents) {
833
833
  "Authorization": `Bearer ${apiKey}`
834
834
  },
835
835
  body: JSON.stringify({
836
- model: "meta-llama/llama-4-scout-17b-16e-instruct",
836
+ model: "gpt-4o-mini",
837
+ // Use reliable model
837
838
  messages: [
838
839
  {
839
840
  role: "system",
840
- 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"}`
841
+ content: `You are a security engineer fixing code vulnerabilities. Generate secure code fixes.
842
+
843
+ IMPORTANT: Respond ONLY with valid JSON in this exact format:
844
+ {"fixedCode": "the complete fixed code snippet", "explanation": "brief explanation of what was changed"}
845
+
846
+ Rules:
847
+ - For hardcoded secrets: Replace with environment variables (process.env.VAR_NAME)
848
+ - For XSS vulnerabilities: Add proper escaping/sanitization
849
+ - For SQL injection: Use parameterized queries
850
+ - For exposed routes: Add authentication middleware
851
+ - Keep the same code structure, only fix the security issue`
841
852
  },
842
853
  {
843
854
  role: "user",
844
855
  content: `Fix this security issue:
845
856
  Type: ${issue.type}
846
857
  Name: ${issue.name}
858
+ Severity: ${issue.severity}
847
859
  File: ${issue.file}:${issue.line}
848
860
 
849
861
  Code to fix:
@@ -851,56 +863,87 @@ Code to fix:
851
863
  ${codeSnippet}
852
864
  \`\`\`
853
865
 
854
- Generate a secure fix.`
866
+ Respond with JSON only.`
855
867
  }
856
868
  ],
857
869
  temperature: 0,
858
- max_tokens: 500
870
+ max_tokens: 1e3
859
871
  })
860
872
  });
861
873
  if (!response.ok) {
862
- throw new Error(`API error: ${response.status}`);
874
+ const errorText = await response.text();
875
+ console.error(`[AI] API error for ${issue.file}:${issue.line}: ${response.status} - ${errorText}`);
876
+ results.push({
877
+ issue,
878
+ originalCode: codeSnippet,
879
+ fixedCode: codeSnippet,
880
+ // Same as original = no fix
881
+ explanation: `API error: ${response.status}`,
882
+ applied: false
883
+ });
884
+ continue;
863
885
  }
864
886
  const data = await response.json();
865
- const content_response = data.choices[0]?.message?.content || "{}";
866
- const parsed = JSON.parse(content_response);
867
- results.push({
868
- issue,
869
- originalCode: codeSnippet,
870
- fixedCode: parsed.fixedCode || codeSnippet,
871
- explanation: parsed.explanation || "No explanation provided",
872
- applied: false
873
- });
874
- } catch {
887
+ if (data.error) {
888
+ console.error(`[AI] API returned error: ${data.error.message}`);
889
+ results.push({
890
+ issue,
891
+ originalCode: codeSnippet,
892
+ fixedCode: codeSnippet,
893
+ explanation: `AI error: ${data.error.message}`,
894
+ applied: false
895
+ });
896
+ continue;
897
+ }
898
+ const content_response = data.choices[0]?.message?.content || "";
899
+ let jsonStr = content_response;
900
+ const jsonMatch = content_response.match(/```(?:json)?\s*([\s\S]*?)```/);
901
+ if (jsonMatch) {
902
+ jsonStr = jsonMatch[1].trim();
903
+ }
904
+ try {
905
+ const parsed = JSON.parse(jsonStr);
906
+ const fixedCode = parsed.fixedCode || parsed.fixed_code || "";
907
+ if (fixedCode && fixedCode !== codeSnippet) {
908
+ results.push({
909
+ issue,
910
+ originalCode: codeSnippet,
911
+ fixedCode,
912
+ explanation: parsed.explanation || "Security fix applied",
913
+ applied: false
914
+ });
915
+ } else {
916
+ results.push({
917
+ issue,
918
+ originalCode: codeSnippet,
919
+ fixedCode: codeSnippet,
920
+ explanation: "AI could not generate a fix for this issue",
921
+ applied: false
922
+ });
923
+ }
924
+ } catch (parseError) {
925
+ console.error(`[AI] JSON parse error for ${issue.file}:${issue.line}:`, parseError);
926
+ results.push({
927
+ issue,
928
+ originalCode: codeSnippet,
929
+ fixedCode: codeSnippet,
930
+ explanation: "Failed to parse AI response",
931
+ applied: false
932
+ });
933
+ }
934
+ } catch (error) {
935
+ console.error(`[AI] Request failed for ${issue.file}:${issue.line}:`, error);
875
936
  results.push({
876
937
  issue,
877
938
  originalCode: codeSnippet,
878
939
  fixedCode: codeSnippet,
879
- explanation: "Unable to generate fix - manual review required",
940
+ explanation: `Request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
880
941
  applied: false
881
942
  });
882
943
  }
883
944
  }
884
945
  return results;
885
946
  }
886
- async function applyFixes(fixes, fileContents) {
887
- for (const fix of fixes) {
888
- if (fix.fixedCode === fix.originalCode) {
889
- continue;
890
- }
891
- const content = fileContents.get(fix.issue.file);
892
- if (!content) {
893
- continue;
894
- }
895
- const newContent = content.replace(fix.originalCode, fix.fixedCode);
896
- if (newContent !== content) {
897
- const filePath = path2.resolve(fix.issue.file);
898
- fs2.writeFileSync(filePath, newContent, "utf-8");
899
- fix.applied = true;
900
- }
901
- }
902
- return fixes;
903
- }
904
947
 
905
948
  // src/telemetry.ts
906
949
  var TELEMETRY_URL = "https://api.cencori.com/v1/telemetry/scan";
@@ -1153,7 +1196,7 @@ No commits found in the specified period.
1153
1196
  // src/cli.ts
1154
1197
  import * as fs3 from "fs";
1155
1198
  import * as path3 from "path";
1156
- var VERSION = "0.4.4";
1199
+ var VERSION = "0.4.6";
1157
1200
  var scoreStyles = {
1158
1201
  A: { color: chalk.green },
1159
1202
  B: { color: chalk.blue },
@@ -1388,57 +1431,137 @@ async function handleAutoFix(result, targetPath) {
1388
1431
  fileContents
1389
1432
  );
1390
1433
  fixSpinner.succeed(`Generated ${fixes.length} fixes`);
1391
- const applySpinner = ora({
1392
- text: "Applying fixes...",
1393
- color: "cyan"
1394
- }).start();
1395
- const appliedFixes = await applyFixes(fixes, fileContents);
1396
- const appliedCount = appliedFixes.filter((f) => f.applied).length;
1397
- applySpinner.succeed(`Applied ${appliedCount}/${fixes.length} fixes`);
1398
1434
  console.log();
1399
- const applied = appliedFixes.filter((f) => f.applied);
1400
- const notApplied = appliedFixes.filter((f) => !f.applied);
1401
- if (applied.length > 0) {
1402
- console.log(` ${chalk.bold.green("\u2714 Applied fixes:")}`);
1403
- for (const fix of applied) {
1404
- console.log(chalk.green(` \u2714 ${fix.issue.file}:${fix.issue.line}`));
1405
- console.log(chalk.gray(` ${fix.explanation}`));
1406
- }
1435
+ const actualFixes = fixes.filter((f) => f.fixedCode !== f.originalCode);
1436
+ const failedFixes = fixes.filter((f) => f.fixedCode === f.originalCode);
1437
+ if (failedFixes.length > 0) {
1438
+ console.log(chalk.gray(` Note: ${failedFixes.length} issues could not be auto-fixed (saved for manual review)`));
1407
1439
  console.log();
1408
1440
  }
1409
- if (notApplied.length > 0) {
1410
- console.log(` ${chalk.bold.yellow("\u26A0 Suggested fixes (require manual review):")}`);
1441
+ if (actualFixes.length === 0) {
1442
+ console.log(chalk.yellow(" No fixes could be generated by AI."));
1443
+ console.log(chalk.gray(" This may be due to API issues or complex code patterns."));
1444
+ const fixesFile = ".cencori-fixes.json";
1445
+ const fixesData = {
1446
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1447
+ total_fixes: failedFixes.length,
1448
+ note: "AI could not generate automatic fixes for these issues",
1449
+ fixes: failedFixes.map((f) => ({
1450
+ file: f.issue.file,
1451
+ line: f.issue.line,
1452
+ issue_type: f.issue.type,
1453
+ issue_name: f.issue.name,
1454
+ severity: f.issue.severity,
1455
+ original_code: f.originalCode,
1456
+ explanation: f.explanation
1457
+ }))
1458
+ };
1459
+ fs3.writeFileSync(fixesFile, JSON.stringify(fixesData, null, 2));
1460
+ console.log(chalk.cyan(` Issues saved to ${chalk.bold(fixesFile)} for manual review.`));
1461
+ console.log();
1462
+ return;
1463
+ }
1464
+ const acceptedFixes = [];
1465
+ const skippedFixes = [];
1466
+ let applyAll = false;
1467
+ let skipRest = false;
1468
+ for (let i = 0; i < actualFixes.length; i++) {
1469
+ const fix = actualFixes[i];
1470
+ if (skipRest) {
1471
+ skippedFixes.push(fix);
1472
+ continue;
1473
+ }
1474
+ if (applyAll) {
1475
+ acceptedFixes.push(fix);
1476
+ continue;
1477
+ }
1478
+ console.log(chalk.cyan(` \u2500\u2500\u2500 Fix ${i + 1}/${fixes.length}: ${fix.issue.file}:${fix.issue.line} \u2500\u2500\u2500`));
1479
+ console.log(chalk.gray(` Issue: ${fix.issue.name} (${fix.issue.severity})`));
1480
+ console.log();
1481
+ const origLines = fix.originalCode.split("\n");
1482
+ console.log(chalk.red(" - Original:"));
1483
+ origLines.slice(0, 8).forEach((line) => console.log(chalk.red(` ${line}`)));
1484
+ if (origLines.length > 8) {
1485
+ console.log(chalk.gray(` ... (${origLines.length - 8} more lines)`));
1486
+ }
1411
1487
  console.log();
1412
- for (let i = 0; i < Math.min(notApplied.length, 5); i++) {
1413
- const fix = notApplied[i];
1414
- console.log(chalk.cyan(` \u2500\u2500\u2500 ${fix.issue.file}:${fix.issue.line} \u2500\u2500\u2500`));
1415
- console.log(chalk.gray(` Issue: ${fix.issue.name}`));
1416
- console.log();
1417
- const origLines = fix.originalCode.split("\n").slice(0, 5);
1418
- console.log(chalk.red(" - Original:"));
1419
- origLines.forEach((line) => console.log(chalk.red(` ${line}`)));
1420
- if (fix.originalCode.split("\n").length > 5) {
1421
- console.log(chalk.gray(" ... (truncated)"));
1488
+ const fixLines = fix.fixedCode.split("\n");
1489
+ console.log(chalk.green(" + Suggested fix:"));
1490
+ fixLines.slice(0, 8).forEach((line) => console.log(chalk.green(` ${line}`)));
1491
+ if (fixLines.length > 8) {
1492
+ console.log(chalk.gray(` ... (${fixLines.length - 8} more lines)`));
1493
+ }
1494
+ console.log();
1495
+ console.log(chalk.gray(` Explanation: ${fix.explanation}`));
1496
+ console.log();
1497
+ const action = await select({
1498
+ message: "Apply this fix?",
1499
+ choices: [
1500
+ { name: "Yes - apply this fix", value: "y" },
1501
+ { name: "No - skip this fix", value: "n" },
1502
+ { name: "All - apply all remaining fixes", value: "a" },
1503
+ { name: "Skip rest - save remaining to file", value: "s" },
1504
+ { name: "Quit - stop reviewing", value: "q" }
1505
+ ]
1506
+ });
1507
+ if (action === "y") {
1508
+ acceptedFixes.push(fix);
1509
+ console.log(chalk.green(" \u2714 Fix accepted"));
1510
+ } else if (action === "n") {
1511
+ skippedFixes.push(fix);
1512
+ console.log(chalk.yellow(" \u2298 Fix skipped"));
1513
+ } else if (action === "a") {
1514
+ applyAll = true;
1515
+ acceptedFixes.push(fix);
1516
+ for (let j = i + 1; j < actualFixes.length; j++) {
1517
+ acceptedFixes.push(actualFixes[j]);
1422
1518
  }
1423
- console.log();
1424
- const fixLines = fix.fixedCode.split("\n").slice(0, 5);
1425
- console.log(chalk.green(" + Suggested fix:"));
1426
- fixLines.forEach((line) => console.log(chalk.green(` ${line}`)));
1427
- if (fix.fixedCode.split("\n").length > 5) {
1428
- console.log(chalk.gray(" ... (truncated)"));
1519
+ console.log(chalk.green(` \u2714 Applying all ${actualFixes.length - i} remaining fixes`));
1520
+ break;
1521
+ } else if (action === "s") {
1522
+ skipRest = true;
1523
+ skippedFixes.push(fix);
1524
+ for (let j = i + 1; j < actualFixes.length; j++) {
1525
+ skippedFixes.push(actualFixes[j]);
1429
1526
  }
1430
- console.log(chalk.gray(` Explanation: ${fix.explanation}`));
1431
- console.log();
1527
+ console.log(chalk.yellow(` \u2298 Skipping ${actualFixes.length - i} remaining fixes`));
1528
+ break;
1529
+ } else if (action === "q") {
1530
+ skippedFixes.push(fix);
1531
+ for (let j = i + 1; j < actualFixes.length; j++) {
1532
+ skippedFixes.push(actualFixes[j]);
1533
+ }
1534
+ console.log(chalk.gray(" Stopped reviewing"));
1535
+ break;
1432
1536
  }
1433
- if (notApplied.length > 5) {
1434
- console.log(chalk.gray(` ... and ${notApplied.length - 5} more fixes`));
1435
- console.log();
1537
+ console.log();
1538
+ }
1539
+ if (acceptedFixes.length > 0) {
1540
+ console.log();
1541
+ const applySpinner = ora({
1542
+ text: `Applying ${acceptedFixes.length} fixes...`,
1543
+ color: "cyan"
1544
+ }).start();
1545
+ let appliedCount = 0;
1546
+ for (const fix of acceptedFixes) {
1547
+ const content = fileContents.get(fix.issue.file);
1548
+ if (!content) continue;
1549
+ const newContent = content.replace(fix.originalCode, fix.fixedCode);
1550
+ if (newContent !== content) {
1551
+ const filePath = path3.resolve(targetPath, fix.issue.file);
1552
+ fs3.writeFileSync(filePath, newContent, "utf-8");
1553
+ fileContents.set(fix.issue.file, newContent);
1554
+ appliedCount++;
1555
+ }
1436
1556
  }
1557
+ applySpinner.succeed(`Applied ${appliedCount}/${acceptedFixes.length} fixes to your codebase`);
1558
+ }
1559
+ if (skippedFixes.length > 0) {
1437
1560
  const fixesFile = ".cencori-fixes.json";
1438
1561
  const fixesData = {
1439
1562
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1440
- total_fixes: notApplied.length,
1441
- fixes: notApplied.map((f) => ({
1563
+ total_fixes: skippedFixes.length,
1564
+ fixes: skippedFixes.map((f) => ({
1442
1565
  file: f.issue.file,
1443
1566
  line: f.issue.line,
1444
1567
  issue_type: f.issue.type,
@@ -1450,16 +1573,22 @@ async function handleAutoFix(result, targetPath) {
1450
1573
  }))
1451
1574
  };
1452
1575
  fs3.writeFileSync(fixesFile, JSON.stringify(fixesData, null, 2));
1453
- 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"));
1454
- console.log();
1455
- console.log(` ${chalk.bold("Next steps:")}`);
1456
- console.log(chalk.cyan(` 1. Review fixes in ${chalk.bold(fixesFile)}`));
1457
- console.log(chalk.cyan(` 2. Apply fixes manually to your codebase`));
1458
- console.log(chalk.cyan(` 3. Run ${chalk.bold("npx @cencori/scan")} again to verify`));
1459
1576
  console.log();
1577
+ console.log(chalk.yellow(` ${skippedFixes.length} skipped fixes saved to ${chalk.bold(fixesFile)}`));
1578
+ }
1579
+ console.log();
1580
+ 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"));
1581
+ console.log();
1582
+ console.log(` ${chalk.bold("Summary:")}`);
1583
+ if (acceptedFixes.length > 0) {
1584
+ console.log(chalk.green(` \u2714 ${acceptedFixes.length} fixes applied`));
1460
1585
  }
1461
- if (applied.length === 0 && notApplied.length === 0) {
1462
- console.log(chalk.green(" No fixes needed!"));
1586
+ if (skippedFixes.length > 0) {
1587
+ console.log(chalk.yellow(` \u2298 ${skippedFixes.length} fixes skipped (saved to .cencori-fixes.json)`));
1588
+ }
1589
+ if (acceptedFixes.length > 0) {
1590
+ console.log();
1591
+ console.log(chalk.cyan(` Run ${chalk.bold("npx @cencori/scan")} again to verify your fixes!`));
1463
1592
  }
1464
1593
  console.log();
1465
1594
  } catch (error) {