@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.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
- var VERSION = "0.2.0";
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 printFixes(issues) {
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
- printFixes(result.issues);
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) {