@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.js +212 -83
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +213 -84
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
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: "
|
|
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.
|
|
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
|
-
|
|
866
|
+
Respond with JSON only.`
|
|
855
867
|
}
|
|
856
868
|
],
|
|
857
869
|
temperature: 0,
|
|
858
|
-
max_tokens:
|
|
870
|
+
max_tokens: 1e3
|
|
859
871
|
})
|
|
860
872
|
});
|
|
861
873
|
if (!response.ok) {
|
|
862
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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:
|
|
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.
|
|
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
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1402
|
-
console.log(` ${
|
|
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 (
|
|
1410
|
-
console.log(
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
console.log();
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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.
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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:
|
|
1441
|
-
fixes:
|
|
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 (
|
|
1462
|
-
console.log(chalk.
|
|
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) {
|