@gulu9527/code-trust 0.2.0 → 0.3.0

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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/core/engine.ts
2
+ import { createHash } from "crypto";
2
3
  import { readFile } from "fs/promises";
3
- import { readFileSync, existsSync as existsSync3 } from "fs";
4
- import { resolve as resolve2, dirname as dirname3 } from "path";
4
+ import { readFileSync } from "fs";
5
+ import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
5
6
  import { fileURLToPath } from "url";
6
7
 
7
8
  // src/parsers/diff.ts
@@ -468,7 +469,7 @@ function detectCodeAfterReturn(context, lines, issues) {
468
469
  }
469
470
  }
470
471
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
471
- const endsOpen = /[{(\[,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
472
+ const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
472
473
  if (endsOpen) continue;
473
474
  lastReturnDepth = braceDepth;
474
475
  lastReturnLine = i;
@@ -524,6 +525,24 @@ function detectImmediateReassign(context, lines, issues) {
524
525
  }
525
526
  }
526
527
 
528
+ // src/rules/fix-utils.ts
529
+ function lineStartOffset(content, lineNumber) {
530
+ let offset = 0;
531
+ const lines = content.split("\n");
532
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
533
+ offset += lines[i].length + 1;
534
+ }
535
+ return offset;
536
+ }
537
+ function lineRange(content, lineNumber) {
538
+ const lines = content.split("\n");
539
+ const lineIndex = lineNumber - 1;
540
+ if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
541
+ const start = lineStartOffset(content, lineNumber);
542
+ const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
543
+ return [start, end];
544
+ }
545
+
527
546
  // src/parsers/ast.ts
528
547
  import { parse, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree";
529
548
 
@@ -706,6 +725,19 @@ var unusedVariablesRule = {
706
725
  severity: "low",
707
726
  title: "Unused variable detected",
708
727
  description: "AI-generated code sometimes declares variables that are never used, indicating incomplete or hallucinated logic.",
728
+ fixable: true,
729
+ fix(context, issue) {
730
+ const lines = context.fileContent.split("\n");
731
+ const lineIndex = issue.startLine - 1;
732
+ if (lineIndex < 0 || lineIndex >= lines.length) return null;
733
+ const line = lines[lineIndex].trim();
734
+ if (/^(const|let|var)\s+\w+\s*[=:;]/.test(line) && !line.includes(",")) {
735
+ const [start, end] = lineRange(context.fileContent, issue.startLine);
736
+ if (start === end) return null;
737
+ return { range: [start, end], text: "" };
738
+ }
739
+ return null;
740
+ },
709
741
  check(context) {
710
742
  const issues = [];
711
743
  let ast;
@@ -872,6 +904,10 @@ function truncate(s, maxLen) {
872
904
  }
873
905
 
874
906
  // src/rules/builtin/security.ts
907
+ var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
908
+ var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
909
+ var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
910
+ var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
875
911
  var securityRules = [
876
912
  {
877
913
  id: "security/hardcoded-secret",
@@ -884,7 +920,7 @@ var securityRules = [
884
920
  const lines = context.fileContent.split("\n");
885
921
  const secretPatterns = [
886
922
  // API keys / tokens
887
- { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_\-]{16,}['"`]/i, label: "API key" },
923
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
888
924
  { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
889
925
  // AWS
890
926
  { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
@@ -937,15 +973,17 @@ var securityRules = [
937
973
  const issues = [];
938
974
  const lines = context.fileContent.split("\n");
939
975
  for (let i = 0; i < lines.length; i++) {
940
- const trimmed = lines[i].trim();
941
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
976
+ const line = lines[i];
977
+ const trimmed = line.trim();
978
+ if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
979
+ const sanitizedLine = stripQuotedStrings(line);
942
980
  const evalPatterns = [
943
- { pattern: /\beval\s*\(/, label: "eval()" },
944
- { pattern: /new\s+Function\s*\(/, label: "new Function()" },
945
- { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
981
+ { pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
982
+ { pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
983
+ { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
946
984
  ];
947
- for (const { pattern, label } of evalPatterns) {
948
- if (pattern.test(lines[i])) {
985
+ for (const { pattern, label, source } of evalPatterns) {
986
+ if (pattern.test(source)) {
949
987
  issues.push({
950
988
  ruleId: "security/eval-usage",
951
989
  severity: "high",
@@ -978,29 +1016,30 @@ var securityRules = [
978
1016
  check(context) {
979
1017
  const issues = [];
980
1018
  const lines = context.fileContent.split("\n");
1019
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
981
1020
  for (let i = 0; i < lines.length; i++) {
982
- const trimmed = lines[i].trim();
983
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
984
- const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
985
- if (sqlKeywords.test(lines[i])) {
986
- if (/\$\{[^}]+\}/.test(lines[i]) || /['"]\s*\+\s*\w+/.test(lines[i])) {
987
- issues.push({
988
- ruleId: "security/sql-injection",
989
- severity: "high",
990
- category: "security",
991
- file: context.filePath,
992
- startLine: i + 1,
993
- endLine: i + 1,
994
- message: t(
995
- "Potential SQL injection \u2014 string interpolation in SQL query.",
996
- "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
997
- ),
998
- suggestion: t(
999
- "Use parameterized queries or prepared statements instead.",
1000
- "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1001
- )
1002
- });
1003
- }
1021
+ const line = lines[i];
1022
+ const trimmed = line.trim();
1023
+ if (isCommentLine(trimmed)) continue;
1024
+ const hasSqlKeyword = sqlKeywords.test(line);
1025
+ const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
1026
+ if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
1027
+ issues.push({
1028
+ ruleId: "security/sql-injection",
1029
+ severity: "high",
1030
+ category: "security",
1031
+ file: context.filePath,
1032
+ startLine: i + 1,
1033
+ endLine: i + 1,
1034
+ message: t(
1035
+ "Potential SQL injection \u2014 string interpolation in SQL query.",
1036
+ "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1037
+ ),
1038
+ suggestion: t(
1039
+ "Use parameterized queries or prepared statements instead.",
1040
+ "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1041
+ )
1042
+ });
1004
1043
  }
1005
1044
  }
1006
1045
  return issues;
@@ -1429,6 +1468,23 @@ var unusedImportRule = {
1429
1468
  severity: "low",
1430
1469
  title: "Unused import",
1431
1470
  description: "AI-generated code often imports modules or identifiers that are never used in the file.",
1471
+ fixable: true,
1472
+ fix(context, issue) {
1473
+ const lines = context.fileContent.split("\n");
1474
+ const lineIndex = issue.startLine - 1;
1475
+ if (lineIndex < 0 || lineIndex >= lines.length) return null;
1476
+ const line = lines[lineIndex].trim();
1477
+ const isSingleDefault = /^import\s+\w+\s+from\s+/.test(line);
1478
+ const isSingleNamed = /^import\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
1479
+ const isSingleTypeNamed = /^import\s+type\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
1480
+ const isSingleTypeDefault = /^import\s+type\s+\w+\s+from\s+/.test(line);
1481
+ if (!isSingleDefault && !isSingleNamed && !isSingleTypeNamed && !isSingleTypeDefault) {
1482
+ return null;
1483
+ }
1484
+ const [start, end] = lineRange(context.fileContent, issue.startLine);
1485
+ if (start === end) return null;
1486
+ return { range: [start, end], text: "" };
1487
+ },
1432
1488
  check(context) {
1433
1489
  const issues = [];
1434
1490
  let ast;
@@ -1666,6 +1722,33 @@ var typeCoercionRule = {
1666
1722
  severity: "medium",
1667
1723
  title: "Loose equality with type coercion",
1668
1724
  description: "AI-generated code often uses == instead of ===, leading to implicit type coercion bugs.",
1725
+ fixable: true,
1726
+ fix(context, issue) {
1727
+ const lines = context.fileContent.split("\n");
1728
+ const lineIndex = issue.startLine - 1;
1729
+ if (lineIndex < 0 || lineIndex >= lines.length) return null;
1730
+ const line = lines[lineIndex];
1731
+ const base = lineStartOffset(context.fileContent, issue.startLine);
1732
+ const isNotEqual = issue.message.includes("!=");
1733
+ const searchOp = isNotEqual ? "!=" : "==";
1734
+ const replaceOp = isNotEqual ? "!==" : "===";
1735
+ let pos = -1;
1736
+ for (let j = 0; j < line.length - 1; j++) {
1737
+ if (line[j] === searchOp[0] && line[j + 1] === "=") {
1738
+ if (line[j + 2] === "=") {
1739
+ j += 2;
1740
+ continue;
1741
+ }
1742
+ if (!isNotEqual && j > 0 && (line[j - 1] === "!" || line[j - 1] === "<" || line[j - 1] === ">")) {
1743
+ continue;
1744
+ }
1745
+ pos = j;
1746
+ break;
1747
+ }
1748
+ }
1749
+ if (pos === -1) return null;
1750
+ return { range: [base + pos, base + pos + searchOp.length], text: replaceOp };
1751
+ },
1669
1752
  check(context) {
1670
1753
  const issues = [];
1671
1754
  const lines = context.fileContent.split("\n");
@@ -1920,6 +2003,12 @@ var noDebuggerRule = {
1920
2003
  severity: "high",
1921
2004
  title: "Debugger statement",
1922
2005
  description: "Debugger statements should never be committed to production code.",
2006
+ fixable: true,
2007
+ fix(context, issue) {
2008
+ const [start, end] = lineRange(context.fileContent, issue.startLine);
2009
+ if (start === end) return null;
2010
+ return { range: [start, end], text: "" };
2011
+ },
1923
2012
  check(context) {
1924
2013
  const issues = [];
1925
2014
  const lines = context.fileContent.split("\n");
@@ -2234,12 +2323,7 @@ var noReassignParamRule = {
2234
2323
  }
2235
2324
  }
2236
2325
  if (paramNames.size === 0) return;
2237
- let body = null;
2238
- if (node.type === AST_NODE_TYPES.MethodDefinition) {
2239
- body = node.value;
2240
- } else {
2241
- body = node;
2242
- }
2326
+ const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
2243
2327
  if (!body || !("body" in body)) return;
2244
2328
  const fnBody = body.body;
2245
2329
  if (!fnBody) return;
@@ -2476,15 +2560,33 @@ var RuleEngine = class {
2476
2560
  );
2477
2561
  }
2478
2562
  run(context) {
2563
+ return this.runWithDiagnostics(context).issues;
2564
+ }
2565
+ runWithDiagnostics(context) {
2479
2566
  const allIssues = [];
2567
+ const ruleFailures = [];
2568
+ let rulesExecuted = 0;
2569
+ let rulesFailed = 0;
2480
2570
  for (const rule of this.rules) {
2571
+ rulesExecuted++;
2481
2572
  try {
2482
2573
  const issues = rule.check(context);
2483
2574
  allIssues.push(...issues);
2484
- } catch (_err) {
2575
+ } catch (err) {
2576
+ rulesFailed++;
2577
+ ruleFailures.push({
2578
+ ruleId: rule.id,
2579
+ file: context.filePath,
2580
+ message: err instanceof Error ? err.message : "Unknown rule execution failure"
2581
+ });
2485
2582
  }
2486
2583
  }
2487
- return allIssues;
2584
+ return {
2585
+ issues: allIssues,
2586
+ rulesExecuted,
2587
+ rulesFailed,
2588
+ ruleFailures
2589
+ };
2488
2590
  }
2489
2591
  getRules() {
2490
2592
  return [...this.rules];
@@ -2864,6 +2966,8 @@ var PKG_VERSION = (() => {
2864
2966
  return "0.1.0";
2865
2967
  }
2866
2968
  })();
2969
+ var REPORT_SCHEMA_VERSION = "1.0.0";
2970
+ var FINGERPRINT_VERSION = "1";
2867
2971
  var ScanEngine = class {
2868
2972
  config;
2869
2973
  diffParser;
@@ -2874,80 +2978,198 @@ var ScanEngine = class {
2874
2978
  this.ruleEngine = new RuleEngine(config);
2875
2979
  }
2876
2980
  async scan(options) {
2877
- const diffFiles = await this.getDiffFiles(options);
2981
+ const selection = await this.getScanCandidates(options);
2878
2982
  const allIssues = [];
2879
- for (const diffFile of diffFiles) {
2880
- if (diffFile.status === "deleted") continue;
2881
- const filePath = resolve2(diffFile.filePath);
2882
- let fileContent;
2883
- try {
2884
- if (existsSync3(filePath)) {
2885
- fileContent = await readFile(filePath, "utf-8");
2886
- } else {
2887
- const content = await this.diffParser.getFileContent(diffFile.filePath);
2888
- if (!content) continue;
2889
- fileContent = content;
2890
- }
2891
- } catch {
2892
- continue;
2893
- }
2894
- const addedLines = diffFile.hunks.flatMap((hunk) => {
2895
- const lines = hunk.content.split("\n");
2896
- const result = [];
2897
- let currentLine = hunk.newStart;
2898
- for (const line of lines) {
2899
- if (line.startsWith("+")) {
2900
- result.push({ lineNumber: currentLine, content: line.slice(1) });
2901
- currentLine++;
2902
- } else if (line.startsWith("-")) {
2903
- } else {
2904
- currentLine++;
2905
- }
2906
- }
2907
- return result;
2908
- });
2909
- const issues = this.ruleEngine.run({
2910
- filePath: diffFile.filePath,
2911
- fileContent,
2912
- addedLines
2913
- });
2914
- allIssues.push(...issues);
2915
- if (this.isTsJsFile(diffFile.filePath)) {
2916
- const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
2917
- maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
2918
- maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
2919
- maxFunctionLength: this.config.thresholds["max-function-length"],
2920
- maxNestingDepth: this.config.thresholds["max-nesting-depth"],
2921
- maxParamCount: this.config.thresholds["max-params"]
2922
- });
2923
- allIssues.push(...structureResult.issues);
2924
- const styleResult = analyzeStyle(fileContent, diffFile.filePath);
2925
- allIssues.push(...styleResult.issues);
2926
- const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
2927
- allIssues.push(...coverageResult.issues);
2928
- }
2929
- }
2930
- const dimensions = this.groupByDimension(allIssues);
2983
+ const scanErrors = [];
2984
+ const ruleFailures = [];
2985
+ let rulesExecuted = 0;
2986
+ let rulesFailed = 0;
2987
+ let filesScanned = 0;
2988
+ const results = await Promise.all(
2989
+ selection.candidates.map((diffFile) => this.scanFile(diffFile))
2990
+ );
2991
+ for (const result of results) {
2992
+ allIssues.push(...result.issues);
2993
+ ruleFailures.push(...result.ruleFailures);
2994
+ scanErrors.push(...result.scanErrors);
2995
+ rulesExecuted += result.rulesExecuted;
2996
+ rulesFailed += result.rulesFailed;
2997
+ if (result.scanned) {
2998
+ filesScanned++;
2999
+ }
3000
+ }
3001
+ const issuesWithFingerprints = this.attachFingerprints(allIssues);
3002
+ const dimensions = this.groupByDimension(issuesWithFingerprints);
2931
3003
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
2932
3004
  const grade = getGrade(overallScore);
2933
3005
  const commitHash = await this.diffParser.getCurrentCommitHash();
2934
3006
  return {
3007
+ schemaVersion: REPORT_SCHEMA_VERSION,
2935
3008
  version: PKG_VERSION,
2936
3009
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2937
3010
  commit: commitHash,
3011
+ scanMode: selection.scanMode,
2938
3012
  overall: {
2939
3013
  score: overallScore,
2940
3014
  grade,
2941
- filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
2942
- issuesFound: allIssues.length
3015
+ filesScanned,
3016
+ issuesFound: issuesWithFingerprints.length
3017
+ },
3018
+ toolHealth: {
3019
+ rulesExecuted,
3020
+ rulesFailed,
3021
+ filesConsidered: selection.filesConsidered,
3022
+ filesScanned,
3023
+ filesExcluded: selection.filesExcluded,
3024
+ filesSkipped: scanErrors.length,
3025
+ scanErrors,
3026
+ ruleFailures
2943
3027
  },
2944
3028
  dimensions,
2945
- issues: allIssues.sort((a, b) => {
3029
+ issues: issuesWithFingerprints.sort((a, b) => {
2946
3030
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
2947
3031
  return severityOrder[a.severity] - severityOrder[b.severity];
2948
3032
  })
2949
3033
  };
2950
3034
  }
3035
+ async scanFile(diffFile) {
3036
+ if (diffFile.status === "deleted") {
3037
+ return {
3038
+ issues: [],
3039
+ ruleFailures: [],
3040
+ rulesExecuted: 0,
3041
+ rulesFailed: 0,
3042
+ scanErrors: [
3043
+ {
3044
+ type: "deleted-file",
3045
+ file: diffFile.filePath,
3046
+ message: `Skipped deleted file: ${diffFile.filePath}`
3047
+ }
3048
+ ],
3049
+ scanned: false
3050
+ };
3051
+ }
3052
+ if (!this.isTsJsFile(diffFile.filePath)) {
3053
+ return {
3054
+ issues: [],
3055
+ ruleFailures: [],
3056
+ rulesExecuted: 0,
3057
+ rulesFailed: 0,
3058
+ scanErrors: [
3059
+ {
3060
+ type: "unsupported-file-type",
3061
+ file: diffFile.filePath,
3062
+ message: `Skipped unsupported file type: ${diffFile.filePath}`
3063
+ }
3064
+ ],
3065
+ scanned: false
3066
+ };
3067
+ }
3068
+ const filePath = resolve2(diffFile.filePath);
3069
+ let fileContent;
3070
+ try {
3071
+ fileContent = await readFile(filePath, "utf-8");
3072
+ } catch {
3073
+ const content = await this.diffParser.getFileContent(diffFile.filePath);
3074
+ if (!content) {
3075
+ return {
3076
+ issues: [],
3077
+ ruleFailures: [],
3078
+ rulesExecuted: 0,
3079
+ rulesFailed: 0,
3080
+ scanErrors: [
3081
+ {
3082
+ type: "missing-file-content",
3083
+ file: diffFile.filePath,
3084
+ message: `Unable to read file content for ${diffFile.filePath}`
3085
+ }
3086
+ ],
3087
+ scanned: false
3088
+ };
3089
+ }
3090
+ fileContent = content;
3091
+ }
3092
+ const addedLines = diffFile.hunks.flatMap((hunk) => {
3093
+ const lines = hunk.content.split("\n");
3094
+ const result = [];
3095
+ let currentLine = hunk.newStart;
3096
+ for (const line of lines) {
3097
+ if (line.startsWith("+")) {
3098
+ result.push({ lineNumber: currentLine, content: line.slice(1) });
3099
+ currentLine++;
3100
+ } else if (line.startsWith("-")) {
3101
+ } else {
3102
+ currentLine++;
3103
+ }
3104
+ }
3105
+ return result;
3106
+ });
3107
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3108
+ filePath: diffFile.filePath,
3109
+ fileContent,
3110
+ addedLines
3111
+ });
3112
+ const issues = [...ruleResult.issues];
3113
+ const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
3114
+ maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3115
+ maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3116
+ maxFunctionLength: this.config.thresholds["max-function-length"],
3117
+ maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3118
+ maxParamCount: this.config.thresholds["max-params"]
3119
+ });
3120
+ issues.push(...structureResult.issues);
3121
+ const styleResult = analyzeStyle(fileContent, diffFile.filePath);
3122
+ issues.push(...styleResult.issues);
3123
+ const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
3124
+ issues.push(...coverageResult.issues);
3125
+ return {
3126
+ issues,
3127
+ ruleFailures: ruleResult.ruleFailures,
3128
+ rulesExecuted: ruleResult.rulesExecuted,
3129
+ rulesFailed: ruleResult.rulesFailed,
3130
+ scanErrors: [],
3131
+ scanned: true
3132
+ };
3133
+ }
3134
+ async getScanCandidates(options) {
3135
+ const scanMode = this.getScanMode(options);
3136
+ const candidates = await this.getDiffFiles(options);
3137
+ if (scanMode === "files") {
3138
+ return {
3139
+ scanMode,
3140
+ candidates,
3141
+ filesConsidered: options.files?.length ?? candidates.length,
3142
+ filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
3143
+ };
3144
+ }
3145
+ const filteredCandidates = [];
3146
+ let filesExcluded = 0;
3147
+ for (const candidate of candidates) {
3148
+ if (this.shouldIncludeFile(candidate.filePath)) {
3149
+ filteredCandidates.push(candidate);
3150
+ } else {
3151
+ filesExcluded++;
3152
+ }
3153
+ }
3154
+ return {
3155
+ scanMode,
3156
+ candidates: filteredCandidates,
3157
+ filesConsidered: candidates.length,
3158
+ filesExcluded
3159
+ };
3160
+ }
3161
+ getScanMode(options) {
3162
+ if (options.staged) {
3163
+ return "staged";
3164
+ }
3165
+ if (options.diff) {
3166
+ return "diff";
3167
+ }
3168
+ if (options.files && options.files.length > 0) {
3169
+ return "files";
3170
+ }
3171
+ return "changed";
3172
+ }
2951
3173
  async getDiffFiles(options) {
2952
3174
  if (options.staged) {
2953
3175
  return this.diffParser.getStagedFiles();
@@ -2956,14 +3178,10 @@ var ScanEngine = class {
2956
3178
  return this.diffParser.getDiffFromRef(options.diff);
2957
3179
  }
2958
3180
  if (options.files && options.files.length > 0) {
3181
+ const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
2959
3182
  return Promise.all(
2960
- options.files.map(async (filePath) => {
2961
- let content = "";
2962
- try {
2963
- content = await readFile(resolve2(filePath), "utf-8");
2964
- } catch {
2965
- }
2966
- return {
3183
+ includedFiles.map(
3184
+ (filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
2967
3185
  filePath,
2968
3186
  status: "modified",
2969
3187
  additions: content.split("\n").length,
@@ -2975,18 +3193,64 @@ var ScanEngine = class {
2975
3193
  oldLines: 0,
2976
3194
  newStart: 1,
2977
3195
  newLines: content.split("\n").length,
2978
- content: content.split("\n").map((l) => "+" + l).join("\n")
3196
+ content: content.split("\n").map((line) => "+" + line).join("\n")
2979
3197
  }
2980
3198
  ]
2981
- };
2982
- })
3199
+ }))
3200
+ )
2983
3201
  );
2984
3202
  }
2985
3203
  return this.diffParser.getChangedFiles();
2986
3204
  }
3205
+ shouldIncludeFile(filePath) {
3206
+ const normalizedPath = filePath.split(sep).join("/");
3207
+ const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
3208
+ const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3209
+ if (!included) {
3210
+ return false;
3211
+ }
3212
+ return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3213
+ }
3214
+ matchesPattern(filePath, pattern) {
3215
+ let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3216
+ regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
3217
+ regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
3218
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*");
3219
+ regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
3220
+ regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
3221
+ return new RegExp(`^${regexPattern}$`).test(filePath);
3222
+ }
2987
3223
  isTsJsFile(filePath) {
2988
3224
  return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
2989
3225
  }
3226
+ attachFingerprints(issues) {
3227
+ const occurrenceCounts = /* @__PURE__ */ new Map();
3228
+ return issues.map((issue) => {
3229
+ const normalizedFile = this.normalizeRelativePath(issue.file);
3230
+ const locationComponent = `${issue.startLine}:${issue.endLine}`;
3231
+ const baseKey = [
3232
+ issue.ruleId,
3233
+ normalizedFile,
3234
+ issue.category,
3235
+ issue.severity,
3236
+ locationComponent
3237
+ ].join("|");
3238
+ const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3239
+ occurrenceCounts.set(baseKey, occurrenceIndex + 1);
3240
+ const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
3241
+ return {
3242
+ ...issue,
3243
+ file: normalizedFile,
3244
+ fingerprint,
3245
+ fingerprintVersion: FINGERPRINT_VERSION
3246
+ };
3247
+ });
3248
+ }
3249
+ normalizeRelativePath(filePath) {
3250
+ const absolutePath = resolve2(filePath);
3251
+ const relativePath = relative(process.cwd(), absolutePath) || filePath;
3252
+ return relativePath.split(sep).join("/");
3253
+ }
2990
3254
  groupByDimension(issues) {
2991
3255
  const categories = [
2992
3256
  "security",
@@ -2997,7 +3261,7 @@ var ScanEngine = class {
2997
3261
  ];
2998
3262
  const grouped = {};
2999
3263
  for (const cat of categories) {
3000
- const catIssues = issues.filter((i) => i.category === cat);
3264
+ const catIssues = issues.filter((issue) => issue.category === cat);
3001
3265
  grouped[cat] = calculateDimensionScore(catIssues);
3002
3266
  }
3003
3267
  return grouped;