@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 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"));
@@ -714,8 +715,220 @@ async function scan(targetPath) {
714
715
  };
715
716
  }
716
717
 
718
+ // src/ai/index.ts
719
+ var fs2 = __toESM(require("fs"));
720
+ var path2 = __toESM(require("path"));
721
+ var os = __toESM(require("os"));
722
+ var CENCORI_API_URL = "https://api.cencori.com/v1";
723
+ var CONFIG_FILE = ".cencorirc";
724
+ function getConfigPath() {
725
+ return path2.join(os.homedir(), CONFIG_FILE);
726
+ }
727
+ function loadApiKeyFromConfig() {
728
+ try {
729
+ const configPath = getConfigPath();
730
+ if (fs2.existsSync(configPath)) {
731
+ const content = fs2.readFileSync(configPath, "utf-8");
732
+ const lines = content.split("\n");
733
+ for (const line of lines) {
734
+ if (line.startsWith("api_key=")) {
735
+ return line.slice("api_key=".length).trim();
736
+ }
737
+ }
738
+ }
739
+ } catch {
740
+ }
741
+ return void 0;
742
+ }
743
+ function saveApiKey(apiKey) {
744
+ const configPath = getConfigPath();
745
+ fs2.writeFileSync(configPath, `api_key=${apiKey}
746
+ `, { mode: 384 });
747
+ }
748
+ function getApiKey() {
749
+ return process.env.CENCORI_API_KEY || loadApiKeyFromConfig();
750
+ }
751
+ var sessionApiKey;
752
+ function setSessionApiKey(apiKey) {
753
+ sessionApiKey = apiKey;
754
+ }
755
+ function getEffectiveApiKey() {
756
+ return sessionApiKey || getApiKey();
757
+ }
758
+ async function validateApiKey(apiKey) {
759
+ try {
760
+ const response = await fetch(`${CENCORI_API_URL}/models`, {
761
+ method: "GET",
762
+ headers: {
763
+ "Authorization": `Bearer ${apiKey}`
764
+ }
765
+ });
766
+ return response.ok;
767
+ } catch {
768
+ return false;
769
+ }
770
+ }
771
+ async function analyzeIssues(issues, fileContents) {
772
+ const apiKey = getEffectiveApiKey();
773
+ if (!apiKey) {
774
+ throw new Error("No API key available");
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 - 3);
781
+ const endLine = Math.min(lines.length, issue.line + 3);
782
+ const context = 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 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"}`
796
+ },
797
+ {
798
+ role: "user",
799
+ content: `Analyze this security finding:
800
+ Type: ${issue.type}
801
+ Name: ${issue.name}
802
+ Match: ${issue.match}
803
+ File: ${issue.file}:${issue.line}
804
+ Context:
805
+ \`\`\`
806
+ ${context}
807
+ \`\`\`
808
+
809
+ Is this a real security issue or a false positive (e.g., test data, example code, documentation)?`
810
+ }
811
+ ],
812
+ temperature: 0,
813
+ max_tokens: 150
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
+ isFalsePositive: parsed.isFalsePositive || false,
825
+ confidence: parsed.confidence || 50,
826
+ reason: parsed.reason || "Unable to analyze"
827
+ });
828
+ } catch {
829
+ results.push({
830
+ issue,
831
+ isFalsePositive: false,
832
+ confidence: 50,
833
+ reason: "Analysis failed - treating as potential issue"
834
+ });
835
+ }
836
+ }
837
+ return results;
838
+ }
839
+ async function generateFixes(issues, fileContents) {
840
+ const apiKey = getEffectiveApiKey();
841
+ if (!apiKey) {
842
+ throw new Error("No API key available");
843
+ }
844
+ const results = [];
845
+ for (const issue of issues) {
846
+ const content = fileContents.get(issue.file) || "";
847
+ const lines = content.split("\n");
848
+ const startLine = Math.max(0, issue.line - 5);
849
+ const endLine = Math.min(lines.length, issue.line + 5);
850
+ const codeSnippet = lines.slice(startLine, endLine).join("\n");
851
+ try {
852
+ const response = await fetch(`${CENCORI_API_URL}/chat/completions`, {
853
+ method: "POST",
854
+ headers: {
855
+ "Content-Type": "application/json",
856
+ "Authorization": `Bearer ${apiKey}`
857
+ },
858
+ body: JSON.stringify({
859
+ model: "gpt-4o-mini",
860
+ messages: [
861
+ {
862
+ role: "system",
863
+ 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"}`
864
+ },
865
+ {
866
+ role: "user",
867
+ content: `Fix this security issue:
868
+ Type: ${issue.type}
869
+ Name: ${issue.name}
870
+ File: ${issue.file}:${issue.line}
871
+
872
+ Code to fix:
873
+ \`\`\`
874
+ ${codeSnippet}
875
+ \`\`\`
876
+
877
+ Generate a secure fix.`
878
+ }
879
+ ],
880
+ temperature: 0,
881
+ max_tokens: 500
882
+ })
883
+ });
884
+ if (!response.ok) {
885
+ throw new Error(`API error: ${response.status}`);
886
+ }
887
+ const data = await response.json();
888
+ const content_response = data.choices[0]?.message?.content || "{}";
889
+ const parsed = JSON.parse(content_response);
890
+ results.push({
891
+ issue,
892
+ originalCode: codeSnippet,
893
+ fixedCode: parsed.fixedCode || codeSnippet,
894
+ explanation: parsed.explanation || "No explanation provided",
895
+ applied: false
896
+ });
897
+ } catch {
898
+ results.push({
899
+ issue,
900
+ originalCode: codeSnippet,
901
+ fixedCode: codeSnippet,
902
+ explanation: "Unable to generate fix - manual review required",
903
+ applied: false
904
+ });
905
+ }
906
+ }
907
+ return results;
908
+ }
909
+ async function applyFixes(fixes, fileContents) {
910
+ for (const fix of fixes) {
911
+ if (fix.fixedCode === fix.originalCode) {
912
+ continue;
913
+ }
914
+ const content = fileContents.get(fix.issue.file);
915
+ if (!content) {
916
+ continue;
917
+ }
918
+ const newContent = content.replace(fix.originalCode, fix.fixedCode);
919
+ if (newContent !== content) {
920
+ const filePath = path2.resolve(fix.issue.file);
921
+ fs2.writeFileSync(filePath, newContent, "utf-8");
922
+ fix.applied = true;
923
+ }
924
+ }
925
+ return fixes;
926
+ }
927
+
717
928
  // src/cli.ts
718
- var VERSION = "0.2.0";
929
+ var fs3 = __toESM(require("fs"));
930
+ var path3 = __toESM(require("path"));
931
+ var VERSION = "0.3.1";
719
932
  var scoreStyles = {
720
933
  A: { color: import_chalk.default.green },
721
934
  B: { color: import_chalk.default.blue },
@@ -808,17 +1021,15 @@ function printSummary(result) {
808
1021
  }
809
1022
  console.log();
810
1023
  }
811
- function printFixes(issues) {
1024
+ function printRecommendations(issues) {
812
1025
  if (issues.length === 0) return;
813
1026
  console.log(` ${import_chalk.default.bold("Recommendations:")}`);
814
1027
  const hasSecrets = issues.some((i) => i.type === "secret");
815
1028
  const hasPII = issues.some((i) => i.type === "pii");
816
1029
  const hasConfig = issues.some((i) => i.type === "config");
817
- const hasVulnerabilities = issues.some((i) => i.type === "vulnerability");
818
1030
  const hasXSS = issues.some((i) => i.category === "xss");
819
1031
  const hasInjection = issues.some((i) => i.category === "injection");
820
1032
  const hasCORS = issues.some((i) => i.category === "cors");
821
- const hasDebug = issues.some((i) => i.category === "debug");
822
1033
  if (hasSecrets) {
823
1034
  console.log(import_chalk.default.gray(" - Use environment variables for secrets"));
824
1035
  console.log(import_chalk.default.gray(" - Never commit API keys to version control"));
@@ -831,19 +1042,13 @@ function printFixes(issues) {
831
1042
  }
832
1043
  if (hasXSS) {
833
1044
  console.log(import_chalk.default.gray(" - Sanitize user input before rendering HTML"));
834
- console.log(import_chalk.default.gray(" - Use textContent instead of innerHTML"));
835
1045
  }
836
1046
  if (hasInjection) {
837
1047
  console.log(import_chalk.default.gray(" - Use parameterized queries for SQL"));
838
- console.log(import_chalk.default.gray(" - Avoid using eval and dynamic code execution"));
839
1048
  }
840
1049
  if (hasCORS) {
841
1050
  console.log(import_chalk.default.gray(" - Configure CORS with specific allowed origins"));
842
1051
  }
843
- if (hasDebug) {
844
- console.log(import_chalk.default.gray(" - Remove console.log statements before production"));
845
- console.log(import_chalk.default.gray(" - Use environment variables for debug flags"));
846
- }
847
1052
  console.log();
848
1053
  }
849
1054
  function printFooter() {
@@ -853,8 +1058,138 @@ function printFooter() {
853
1058
  console.log(` Docs: ${import_chalk.default.cyan("https://cencori.com/docs")}`);
854
1059
  console.log();
855
1060
  }
1061
+ function loadFileContents(issues, basePath) {
1062
+ const contents = /* @__PURE__ */ new Map();
1063
+ const uniqueFiles = [...new Set(issues.map((i) => i.file))];
1064
+ for (const file of uniqueFiles) {
1065
+ try {
1066
+ const fullPath = path3.resolve(basePath, file);
1067
+ const content = fs3.readFileSync(fullPath, "utf-8");
1068
+ contents.set(file, content);
1069
+ } catch {
1070
+ }
1071
+ }
1072
+ return contents;
1073
+ }
1074
+ async function promptForApiKey() {
1075
+ console.log();
1076
+ 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"));
1077
+ console.log();
1078
+ console.log(` ${import_chalk.default.cyan.bold("Cencori Pro")}`);
1079
+ console.log(import_chalk.default.gray(" AI-powered auto-fix requires an API key."));
1080
+ console.log();
1081
+ console.log(` Get your free API key at:`);
1082
+ console.log(` ${import_chalk.default.cyan("https://cencori.com/dashboard")} \u2192 API Keys`);
1083
+ console.log();
1084
+ try {
1085
+ const apiKey = await (0, import_prompts.password)({
1086
+ message: "Enter your Cencori API key:",
1087
+ mask: "*"
1088
+ });
1089
+ if (!apiKey || apiKey.trim() === "") {
1090
+ console.log(import_chalk.default.yellow(" No API key entered. Skipping auto-fix."));
1091
+ return void 0;
1092
+ }
1093
+ return apiKey.trim();
1094
+ } catch {
1095
+ return void 0;
1096
+ }
1097
+ }
1098
+ async function handleAutoFix(result, targetPath) {
1099
+ if (result.issues.length === 0) return;
1100
+ console.log();
1101
+ const shouldFix = await (0, import_prompts.confirm)({
1102
+ message: "Would you like Cencori to auto-fix these issues?",
1103
+ default: false
1104
+ });
1105
+ if (!shouldFix) {
1106
+ console.log();
1107
+ console.log(import_chalk.default.gray(" Skipped auto-fix. Run again anytime to fix issues."));
1108
+ console.log();
1109
+ return;
1110
+ }
1111
+ let apiKey = getApiKey();
1112
+ if (!apiKey) {
1113
+ apiKey = await promptForApiKey();
1114
+ if (!apiKey) {
1115
+ console.log();
1116
+ return;
1117
+ }
1118
+ const validatingSpinner = (0, import_ora.default)({
1119
+ text: "Validating API key...",
1120
+ color: "cyan"
1121
+ }).start();
1122
+ const isValid = await validateApiKey(apiKey);
1123
+ if (!isValid) {
1124
+ validatingSpinner.fail("Invalid API key");
1125
+ console.log(import_chalk.default.red(" The API key could not be validated. Please check and try again."));
1126
+ console.log();
1127
+ return;
1128
+ }
1129
+ validatingSpinner.succeed("API key validated");
1130
+ try {
1131
+ saveApiKey(apiKey);
1132
+ console.log(import_chalk.default.green(" \u2714 API key saved to ~/.cencorirc"));
1133
+ } catch {
1134
+ }
1135
+ setSessionApiKey(apiKey);
1136
+ } else {
1137
+ console.log(import_chalk.default.gray(" Using saved API key..."));
1138
+ }
1139
+ const fileContents = loadFileContents(result.issues, targetPath);
1140
+ const analyzeSpinner = (0, import_ora.default)({
1141
+ text: "Analyzing issues with AI...",
1142
+ color: "cyan"
1143
+ }).start();
1144
+ try {
1145
+ const analysis = await analyzeIssues(result.issues, fileContents);
1146
+ const realIssues = analysis.filter((a) => !a.isFalsePositive);
1147
+ const falsePositives = analysis.filter((a) => a.isFalsePositive);
1148
+ if (falsePositives.length > 0) {
1149
+ analyzeSpinner.succeed(`${import_chalk.default.green(falsePositives.length)} false positives filtered`);
1150
+ } else {
1151
+ analyzeSpinner.succeed("Analysis complete");
1152
+ }
1153
+ if (realIssues.length === 0) {
1154
+ console.log(import_chalk.default.green(" All issues were false positives!"));
1155
+ return;
1156
+ }
1157
+ const fixSpinner = (0, import_ora.default)({
1158
+ text: "Generating fixes...",
1159
+ color: "cyan"
1160
+ }).start();
1161
+ const fixes = await generateFixes(
1162
+ realIssues.map((a) => a.issue),
1163
+ fileContents
1164
+ );
1165
+ fixSpinner.succeed(`Generated ${fixes.length} fixes`);
1166
+ const applySpinner = (0, import_ora.default)({
1167
+ text: "Applying fixes...",
1168
+ color: "cyan"
1169
+ }).start();
1170
+ const appliedFixes = await applyFixes(fixes, fileContents);
1171
+ const appliedCount = appliedFixes.filter((f) => f.applied).length;
1172
+ applySpinner.succeed(`Applied ${appliedCount}/${fixes.length} fixes`);
1173
+ console.log();
1174
+ console.log(` ${import_chalk.default.bold("Applied fixes:")}`);
1175
+ for (const fix of appliedFixes.filter((f) => f.applied)) {
1176
+ console.log(import_chalk.default.green(` \u2714 ${fix.issue.file}:${fix.issue.line}`));
1177
+ console.log(import_chalk.default.gray(` ${fix.explanation}`));
1178
+ }
1179
+ const notApplied = appliedFixes.filter((f) => !f.applied);
1180
+ if (notApplied.length > 0) {
1181
+ console.log();
1182
+ console.log(` ${import_chalk.default.yellow(`${notApplied.length} issues require manual review`)}`);
1183
+ }
1184
+ console.log();
1185
+ } catch (error) {
1186
+ analyzeSpinner.fail("Auto-fix failed");
1187
+ console.error(import_chalk.default.red(` Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1188
+ console.log();
1189
+ }
1190
+ }
856
1191
  async function main() {
857
- 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) => {
1192
+ 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) => {
858
1193
  if (options.json) {
859
1194
  const result = await scan(targetPath);
860
1195
  console.log(JSON.stringify(result, null, 2));
@@ -880,7 +1215,10 @@ async function main() {
880
1215
  printScore(result);
881
1216
  printIssues(result.issues);
882
1217
  printSummary(result);
883
- printFixes(result.issues);
1218
+ printRecommendations(result.issues);
1219
+ if (options.prompt !== false && result.issues.length > 0) {
1220
+ await handleAutoFix(result, targetPath);
1221
+ }
884
1222
  printFooter();
885
1223
  process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
886
1224
  } catch (error) {