@cencori/scan 0.2.0 → 0.3.1
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 +350 -12
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +350 -12
- package/dist/cli.mjs.map +1 -1
- package/package.json +5 -3
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, password } from "@inquirer/prompts";
|
|
7
8
|
|
|
8
9
|
// src/scanner/index.ts
|
|
9
10
|
import * as fs from "fs";
|
|
@@ -691,8 +692,220 @@ async function scan(targetPath) {
|
|
|
691
692
|
};
|
|
692
693
|
}
|
|
693
694
|
|
|
695
|
+
// src/ai/index.ts
|
|
696
|
+
import * as fs2 from "fs";
|
|
697
|
+
import * as path2 from "path";
|
|
698
|
+
import * as os from "os";
|
|
699
|
+
var CENCORI_API_URL = "https://api.cencori.com/v1";
|
|
700
|
+
var CONFIG_FILE = ".cencorirc";
|
|
701
|
+
function getConfigPath() {
|
|
702
|
+
return path2.join(os.homedir(), CONFIG_FILE);
|
|
703
|
+
}
|
|
704
|
+
function loadApiKeyFromConfig() {
|
|
705
|
+
try {
|
|
706
|
+
const configPath = getConfigPath();
|
|
707
|
+
if (fs2.existsSync(configPath)) {
|
|
708
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
709
|
+
const lines = content.split("\n");
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
if (line.startsWith("api_key=")) {
|
|
712
|
+
return line.slice("api_key=".length).trim();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
return void 0;
|
|
719
|
+
}
|
|
720
|
+
function saveApiKey(apiKey) {
|
|
721
|
+
const configPath = getConfigPath();
|
|
722
|
+
fs2.writeFileSync(configPath, `api_key=${apiKey}
|
|
723
|
+
`, { mode: 384 });
|
|
724
|
+
}
|
|
725
|
+
function getApiKey() {
|
|
726
|
+
return process.env.CENCORI_API_KEY || loadApiKeyFromConfig();
|
|
727
|
+
}
|
|
728
|
+
var sessionApiKey;
|
|
729
|
+
function setSessionApiKey(apiKey) {
|
|
730
|
+
sessionApiKey = apiKey;
|
|
731
|
+
}
|
|
732
|
+
function getEffectiveApiKey() {
|
|
733
|
+
return sessionApiKey || getApiKey();
|
|
734
|
+
}
|
|
735
|
+
async function validateApiKey(apiKey) {
|
|
736
|
+
try {
|
|
737
|
+
const response = await fetch(`${CENCORI_API_URL}/models`, {
|
|
738
|
+
method: "GET",
|
|
739
|
+
headers: {
|
|
740
|
+
"Authorization": `Bearer ${apiKey}`
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
return response.ok;
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async function analyzeIssues(issues, fileContents) {
|
|
749
|
+
const apiKey = getEffectiveApiKey();
|
|
750
|
+
if (!apiKey) {
|
|
751
|
+
throw new Error("No API key available");
|
|
752
|
+
}
|
|
753
|
+
const results = [];
|
|
754
|
+
for (const issue of issues) {
|
|
755
|
+
const content = fileContents.get(issue.file) || "";
|
|
756
|
+
const lines = content.split("\n");
|
|
757
|
+
const startLine = Math.max(0, issue.line - 3);
|
|
758
|
+
const endLine = Math.min(lines.length, issue.line + 3);
|
|
759
|
+
const context = lines.slice(startLine, endLine).join("\n");
|
|
760
|
+
try {
|
|
761
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
762
|
+
method: "POST",
|
|
763
|
+
headers: {
|
|
764
|
+
"Content-Type": "application/json",
|
|
765
|
+
"Authorization": `Bearer ${apiKey}`
|
|
766
|
+
},
|
|
767
|
+
body: JSON.stringify({
|
|
768
|
+
model: "gpt-4o-mini",
|
|
769
|
+
messages: [
|
|
770
|
+
{
|
|
771
|
+
role: "system",
|
|
772
|
+
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"}`
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
role: "user",
|
|
776
|
+
content: `Analyze this security finding:
|
|
777
|
+
Type: ${issue.type}
|
|
778
|
+
Name: ${issue.name}
|
|
779
|
+
Match: ${issue.match}
|
|
780
|
+
File: ${issue.file}:${issue.line}
|
|
781
|
+
Context:
|
|
782
|
+
\`\`\`
|
|
783
|
+
${context}
|
|
784
|
+
\`\`\`
|
|
785
|
+
|
|
786
|
+
Is this a real security issue or a false positive (e.g., test data, example code, documentation)?`
|
|
787
|
+
}
|
|
788
|
+
],
|
|
789
|
+
temperature: 0,
|
|
790
|
+
max_tokens: 150
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
if (!response.ok) {
|
|
794
|
+
throw new Error(`API error: ${response.status}`);
|
|
795
|
+
}
|
|
796
|
+
const data = await response.json();
|
|
797
|
+
const content_response = data.choices[0]?.message?.content || "{}";
|
|
798
|
+
const parsed = JSON.parse(content_response);
|
|
799
|
+
results.push({
|
|
800
|
+
issue,
|
|
801
|
+
isFalsePositive: parsed.isFalsePositive || false,
|
|
802
|
+
confidence: parsed.confidence || 50,
|
|
803
|
+
reason: parsed.reason || "Unable to analyze"
|
|
804
|
+
});
|
|
805
|
+
} catch {
|
|
806
|
+
results.push({
|
|
807
|
+
issue,
|
|
808
|
+
isFalsePositive: false,
|
|
809
|
+
confidence: 50,
|
|
810
|
+
reason: "Analysis failed - treating as potential issue"
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
async function generateFixes(issues, fileContents) {
|
|
817
|
+
const apiKey = getEffectiveApiKey();
|
|
818
|
+
if (!apiKey) {
|
|
819
|
+
throw new Error("No API key available");
|
|
820
|
+
}
|
|
821
|
+
const results = [];
|
|
822
|
+
for (const issue of issues) {
|
|
823
|
+
const content = fileContents.get(issue.file) || "";
|
|
824
|
+
const lines = content.split("\n");
|
|
825
|
+
const startLine = Math.max(0, issue.line - 5);
|
|
826
|
+
const endLine = Math.min(lines.length, issue.line + 5);
|
|
827
|
+
const codeSnippet = lines.slice(startLine, endLine).join("\n");
|
|
828
|
+
try {
|
|
829
|
+
const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: {
|
|
832
|
+
"Content-Type": "application/json",
|
|
833
|
+
"Authorization": `Bearer ${apiKey}`
|
|
834
|
+
},
|
|
835
|
+
body: JSON.stringify({
|
|
836
|
+
model: "gpt-4o-mini",
|
|
837
|
+
messages: [
|
|
838
|
+
{
|
|
839
|
+
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
|
+
},
|
|
842
|
+
{
|
|
843
|
+
role: "user",
|
|
844
|
+
content: `Fix this security issue:
|
|
845
|
+
Type: ${issue.type}
|
|
846
|
+
Name: ${issue.name}
|
|
847
|
+
File: ${issue.file}:${issue.line}
|
|
848
|
+
|
|
849
|
+
Code to fix:
|
|
850
|
+
\`\`\`
|
|
851
|
+
${codeSnippet}
|
|
852
|
+
\`\`\`
|
|
853
|
+
|
|
854
|
+
Generate a secure fix.`
|
|
855
|
+
}
|
|
856
|
+
],
|
|
857
|
+
temperature: 0,
|
|
858
|
+
max_tokens: 500
|
|
859
|
+
})
|
|
860
|
+
});
|
|
861
|
+
if (!response.ok) {
|
|
862
|
+
throw new Error(`API error: ${response.status}`);
|
|
863
|
+
}
|
|
864
|
+
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 {
|
|
875
|
+
results.push({
|
|
876
|
+
issue,
|
|
877
|
+
originalCode: codeSnippet,
|
|
878
|
+
fixedCode: codeSnippet,
|
|
879
|
+
explanation: "Unable to generate fix - manual review required",
|
|
880
|
+
applied: false
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return results;
|
|
885
|
+
}
|
|
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
|
+
|
|
694
905
|
// src/cli.ts
|
|
695
|
-
|
|
906
|
+
import * as fs3 from "fs";
|
|
907
|
+
import * as path3 from "path";
|
|
908
|
+
var VERSION = "0.3.1";
|
|
696
909
|
var scoreStyles = {
|
|
697
910
|
A: { color: chalk.green },
|
|
698
911
|
B: { color: chalk.blue },
|
|
@@ -785,17 +998,15 @@ function printSummary(result) {
|
|
|
785
998
|
}
|
|
786
999
|
console.log();
|
|
787
1000
|
}
|
|
788
|
-
function
|
|
1001
|
+
function printRecommendations(issues) {
|
|
789
1002
|
if (issues.length === 0) return;
|
|
790
1003
|
console.log(` ${chalk.bold("Recommendations:")}`);
|
|
791
1004
|
const hasSecrets = issues.some((i) => i.type === "secret");
|
|
792
1005
|
const hasPII = issues.some((i) => i.type === "pii");
|
|
793
1006
|
const hasConfig = issues.some((i) => i.type === "config");
|
|
794
|
-
const hasVulnerabilities = issues.some((i) => i.type === "vulnerability");
|
|
795
1007
|
const hasXSS = issues.some((i) => i.category === "xss");
|
|
796
1008
|
const hasInjection = issues.some((i) => i.category === "injection");
|
|
797
1009
|
const hasCORS = issues.some((i) => i.category === "cors");
|
|
798
|
-
const hasDebug = issues.some((i) => i.category === "debug");
|
|
799
1010
|
if (hasSecrets) {
|
|
800
1011
|
console.log(chalk.gray(" - Use environment variables for secrets"));
|
|
801
1012
|
console.log(chalk.gray(" - Never commit API keys to version control"));
|
|
@@ -808,19 +1019,13 @@ function printFixes(issues) {
|
|
|
808
1019
|
}
|
|
809
1020
|
if (hasXSS) {
|
|
810
1021
|
console.log(chalk.gray(" - Sanitize user input before rendering HTML"));
|
|
811
|
-
console.log(chalk.gray(" - Use textContent instead of innerHTML"));
|
|
812
1022
|
}
|
|
813
1023
|
if (hasInjection) {
|
|
814
1024
|
console.log(chalk.gray(" - Use parameterized queries for SQL"));
|
|
815
|
-
console.log(chalk.gray(" - Avoid using eval and dynamic code execution"));
|
|
816
1025
|
}
|
|
817
1026
|
if (hasCORS) {
|
|
818
1027
|
console.log(chalk.gray(" - Configure CORS with specific allowed origins"));
|
|
819
1028
|
}
|
|
820
|
-
if (hasDebug) {
|
|
821
|
-
console.log(chalk.gray(" - Remove console.log statements before production"));
|
|
822
|
-
console.log(chalk.gray(" - Use environment variables for debug flags"));
|
|
823
|
-
}
|
|
824
1029
|
console.log();
|
|
825
1030
|
}
|
|
826
1031
|
function printFooter() {
|
|
@@ -830,8 +1035,138 @@ function printFooter() {
|
|
|
830
1035
|
console.log(` Docs: ${chalk.cyan("https://cencori.com/docs")}`);
|
|
831
1036
|
console.log();
|
|
832
1037
|
}
|
|
1038
|
+
function loadFileContents(issues, basePath) {
|
|
1039
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1040
|
+
const uniqueFiles = [...new Set(issues.map((i) => i.file))];
|
|
1041
|
+
for (const file of uniqueFiles) {
|
|
1042
|
+
try {
|
|
1043
|
+
const fullPath = path3.resolve(basePath, file);
|
|
1044
|
+
const content = fs3.readFileSync(fullPath, "utf-8");
|
|
1045
|
+
contents.set(file, content);
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return contents;
|
|
1050
|
+
}
|
|
1051
|
+
async function promptForApiKey() {
|
|
1052
|
+
console.log();
|
|
1053
|
+
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"));
|
|
1054
|
+
console.log();
|
|
1055
|
+
console.log(` ${chalk.cyan.bold("Cencori Pro")}`);
|
|
1056
|
+
console.log(chalk.gray(" AI-powered auto-fix requires an API key."));
|
|
1057
|
+
console.log();
|
|
1058
|
+
console.log(` Get your free API key at:`);
|
|
1059
|
+
console.log(` ${chalk.cyan("https://cencori.com/dashboard")} \u2192 API Keys`);
|
|
1060
|
+
console.log();
|
|
1061
|
+
try {
|
|
1062
|
+
const apiKey = await password({
|
|
1063
|
+
message: "Enter your Cencori API key:",
|
|
1064
|
+
mask: "*"
|
|
1065
|
+
});
|
|
1066
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
1067
|
+
console.log(chalk.yellow(" No API key entered. Skipping auto-fix."));
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
1070
|
+
return apiKey.trim();
|
|
1071
|
+
} catch {
|
|
1072
|
+
return void 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async function handleAutoFix(result, targetPath) {
|
|
1076
|
+
if (result.issues.length === 0) return;
|
|
1077
|
+
console.log();
|
|
1078
|
+
const shouldFix = await confirm({
|
|
1079
|
+
message: "Would you like Cencori to auto-fix these issues?",
|
|
1080
|
+
default: false
|
|
1081
|
+
});
|
|
1082
|
+
if (!shouldFix) {
|
|
1083
|
+
console.log();
|
|
1084
|
+
console.log(chalk.gray(" Skipped auto-fix. Run again anytime to fix issues."));
|
|
1085
|
+
console.log();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
let apiKey = getApiKey();
|
|
1089
|
+
if (!apiKey) {
|
|
1090
|
+
apiKey = await promptForApiKey();
|
|
1091
|
+
if (!apiKey) {
|
|
1092
|
+
console.log();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const validatingSpinner = ora({
|
|
1096
|
+
text: "Validating API key...",
|
|
1097
|
+
color: "cyan"
|
|
1098
|
+
}).start();
|
|
1099
|
+
const isValid = await validateApiKey(apiKey);
|
|
1100
|
+
if (!isValid) {
|
|
1101
|
+
validatingSpinner.fail("Invalid API key");
|
|
1102
|
+
console.log(chalk.red(" The API key could not be validated. Please check and try again."));
|
|
1103
|
+
console.log();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
validatingSpinner.succeed("API key validated");
|
|
1107
|
+
try {
|
|
1108
|
+
saveApiKey(apiKey);
|
|
1109
|
+
console.log(chalk.green(" \u2714 API key saved to ~/.cencorirc"));
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
setSessionApiKey(apiKey);
|
|
1113
|
+
} else {
|
|
1114
|
+
console.log(chalk.gray(" Using saved API key..."));
|
|
1115
|
+
}
|
|
1116
|
+
const fileContents = loadFileContents(result.issues, targetPath);
|
|
1117
|
+
const analyzeSpinner = ora({
|
|
1118
|
+
text: "Analyzing issues with AI...",
|
|
1119
|
+
color: "cyan"
|
|
1120
|
+
}).start();
|
|
1121
|
+
try {
|
|
1122
|
+
const analysis = await analyzeIssues(result.issues, fileContents);
|
|
1123
|
+
const realIssues = analysis.filter((a) => !a.isFalsePositive);
|
|
1124
|
+
const falsePositives = analysis.filter((a) => a.isFalsePositive);
|
|
1125
|
+
if (falsePositives.length > 0) {
|
|
1126
|
+
analyzeSpinner.succeed(`${chalk.green(falsePositives.length)} false positives filtered`);
|
|
1127
|
+
} else {
|
|
1128
|
+
analyzeSpinner.succeed("Analysis complete");
|
|
1129
|
+
}
|
|
1130
|
+
if (realIssues.length === 0) {
|
|
1131
|
+
console.log(chalk.green(" All issues were false positives!"));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const fixSpinner = ora({
|
|
1135
|
+
text: "Generating fixes...",
|
|
1136
|
+
color: "cyan"
|
|
1137
|
+
}).start();
|
|
1138
|
+
const fixes = await generateFixes(
|
|
1139
|
+
realIssues.map((a) => a.issue),
|
|
1140
|
+
fileContents
|
|
1141
|
+
);
|
|
1142
|
+
fixSpinner.succeed(`Generated ${fixes.length} fixes`);
|
|
1143
|
+
const applySpinner = ora({
|
|
1144
|
+
text: "Applying fixes...",
|
|
1145
|
+
color: "cyan"
|
|
1146
|
+
}).start();
|
|
1147
|
+
const appliedFixes = await applyFixes(fixes, fileContents);
|
|
1148
|
+
const appliedCount = appliedFixes.filter((f) => f.applied).length;
|
|
1149
|
+
applySpinner.succeed(`Applied ${appliedCount}/${fixes.length} fixes`);
|
|
1150
|
+
console.log();
|
|
1151
|
+
console.log(` ${chalk.bold("Applied fixes:")}`);
|
|
1152
|
+
for (const fix of appliedFixes.filter((f) => f.applied)) {
|
|
1153
|
+
console.log(chalk.green(` \u2714 ${fix.issue.file}:${fix.issue.line}`));
|
|
1154
|
+
console.log(chalk.gray(` ${fix.explanation}`));
|
|
1155
|
+
}
|
|
1156
|
+
const notApplied = appliedFixes.filter((f) => !f.applied);
|
|
1157
|
+
if (notApplied.length > 0) {
|
|
1158
|
+
console.log();
|
|
1159
|
+
console.log(` ${chalk.yellow(`${notApplied.length} issues require manual review`)}`);
|
|
1160
|
+
}
|
|
1161
|
+
console.log();
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
analyzeSpinner.fail("Auto-fix failed");
|
|
1164
|
+
console.error(chalk.red(` Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
1165
|
+
console.log();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
833
1168
|
async function main() {
|
|
834
|
-
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) => {
|
|
1169
|
+
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) => {
|
|
835
1170
|
if (options.json) {
|
|
836
1171
|
const result = await scan(targetPath);
|
|
837
1172
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -857,7 +1192,10 @@ async function main() {
|
|
|
857
1192
|
printScore(result);
|
|
858
1193
|
printIssues(result.issues);
|
|
859
1194
|
printSummary(result);
|
|
860
|
-
|
|
1195
|
+
printRecommendations(result.issues);
|
|
1196
|
+
if (options.prompt !== false && result.issues.length > 0) {
|
|
1197
|
+
await handleAutoFix(result, targetPath);
|
|
1198
|
+
}
|
|
861
1199
|
printFooter();
|
|
862
1200
|
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
863
1201
|
} catch (error) {
|