@gulu9527/code-trust 0.2.1 → 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.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  type Severity = 'high' | 'medium' | 'low' | 'info';
2
2
  type RuleCategory = 'security' | 'logic' | 'structure' | 'style' | 'coverage';
3
3
  type TrustGrade = 'HIGH_TRUST' | 'REVIEW' | 'LOW_TRUST' | 'UNTRUSTED';
4
+ type ScanMode = 'staged' | 'diff' | 'files' | 'changed';
5
+ type ScanErrorType = 'rule-failure' | 'deleted-file' | 'unreadable-file' | 'missing-file-content' | 'unsupported-file-type';
4
6
  interface Issue {
5
7
  ruleId: string;
6
8
  severity: Severity;
@@ -12,20 +14,54 @@ interface Issue {
12
14
  suggestion?: string;
13
15
  codeSnippet?: string;
14
16
  }
17
+ interface ReportIssue extends Issue {
18
+ fingerprint: string;
19
+ fingerprintVersion: string;
20
+ }
15
21
  interface DimensionScore {
16
22
  score: number;
23
+ issues: ReportIssue[];
24
+ }
25
+ interface RuleFailure {
26
+ ruleId: string;
27
+ file: string;
28
+ message: string;
29
+ }
30
+ interface RuleRunResult {
17
31
  issues: Issue[];
32
+ rulesExecuted: number;
33
+ rulesFailed: number;
34
+ ruleFailures: RuleFailure[];
35
+ }
36
+ interface ScanError {
37
+ type: ScanErrorType;
38
+ message: string;
39
+ file?: string;
40
+ ruleId?: string;
41
+ }
42
+ interface ToolHealth {
43
+ rulesExecuted: number;
44
+ rulesFailed: number;
45
+ filesConsidered: number;
46
+ filesScanned: number;
47
+ filesExcluded: number;
48
+ filesSkipped: number;
49
+ scanErrors: ScanError[];
50
+ ruleFailures: RuleFailure[];
18
51
  }
19
52
  interface TrustReport {
53
+ schemaVersion: string;
20
54
  version: string;
21
55
  timestamp: string;
22
56
  commit?: string;
57
+ scanMode: ScanMode;
23
58
  overall: {
24
59
  score: number;
25
60
  grade: TrustGrade;
26
61
  filesScanned: number;
27
62
  issuesFound: number;
28
63
  };
64
+ toolHealth: ToolHealth;
29
65
  dimensions: {
30
66
  security: DimensionScore;
31
67
  logic: DimensionScore;
@@ -33,7 +69,7 @@ interface TrustReport {
33
69
  style: DimensionScore;
34
70
  coverage: DimensionScore;
35
71
  };
36
- issues: Issue[];
72
+ issues: ReportIssue[];
37
73
  }
38
74
  interface DiffFile {
39
75
  filePath: string;
@@ -97,8 +133,15 @@ declare class ScanEngine {
97
133
  private ruleEngine;
98
134
  constructor(config: CodeTrustConfig, workDir?: string);
99
135
  scan(options: ScanOptions): Promise<TrustReport>;
136
+ private scanFile;
137
+ private getScanCandidates;
138
+ private getScanMode;
100
139
  private getDiffFiles;
140
+ private shouldIncludeFile;
141
+ private matchesPattern;
101
142
  private isTsJsFile;
143
+ private attachFingerprints;
144
+ private normalizeRelativePath;
102
145
  private groupByDimension;
103
146
  }
104
147
 
@@ -137,6 +180,7 @@ declare class RuleEngine {
137
180
  private rules;
138
181
  constructor(config: CodeTrustConfig);
139
182
  run(context: RuleContext): Issue[];
183
+ runWithDiagnostics(context: RuleContext): RuleRunResult;
140
184
  getRules(): Rule[];
141
185
  listRules(): Array<{
142
186
  id: string;
@@ -160,7 +204,7 @@ declare class DiffParser {
160
204
  private parseHunks;
161
205
  }
162
206
 
163
- declare function calculateDimensionScore(issues: Issue[]): DimensionScore;
207
+ declare function calculateDimensionScore(issues: ReportIssue[]): DimensionScore;
164
208
  declare function calculateOverallScore(dimensions: Record<RuleCategory, DimensionScore>, weights: DimensionWeights): number;
165
209
  declare function getGrade(score: number): TrustGrade;
166
210
  declare function getGradeEmoji(grade: TrustGrade): string;
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;
@@ -903,6 +904,10 @@ function truncate(s, maxLen) {
903
904
  }
904
905
 
905
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);
906
911
  var securityRules = [
907
912
  {
908
913
  id: "security/hardcoded-secret",
@@ -915,7 +920,7 @@ var securityRules = [
915
920
  const lines = context.fileContent.split("\n");
916
921
  const secretPatterns = [
917
922
  // API keys / tokens
918
- { 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" },
919
924
  { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
920
925
  // AWS
921
926
  { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
@@ -968,15 +973,17 @@ var securityRules = [
968
973
  const issues = [];
969
974
  const lines = context.fileContent.split("\n");
970
975
  for (let i = 0; i < lines.length; i++) {
971
- const trimmed = lines[i].trim();
972
- 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);
973
980
  const evalPatterns = [
974
- { pattern: /\beval\s*\(/, label: "eval()" },
975
- { pattern: /new\s+Function\s*\(/, label: "new Function()" },
976
- { 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 }
977
984
  ];
978
- for (const { pattern, label } of evalPatterns) {
979
- if (pattern.test(lines[i])) {
985
+ for (const { pattern, label, source } of evalPatterns) {
986
+ if (pattern.test(source)) {
980
987
  issues.push({
981
988
  ruleId: "security/eval-usage",
982
989
  severity: "high",
@@ -1009,29 +1016,30 @@ var securityRules = [
1009
1016
  check(context) {
1010
1017
  const issues = [];
1011
1018
  const lines = context.fileContent.split("\n");
1019
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1012
1020
  for (let i = 0; i < lines.length; i++) {
1013
- const trimmed = lines[i].trim();
1014
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1015
- const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1016
- if (sqlKeywords.test(lines[i])) {
1017
- if (/\$\{[^}]+\}/.test(lines[i]) || /['"]\s*\+\s*\w+/.test(lines[i])) {
1018
- issues.push({
1019
- ruleId: "security/sql-injection",
1020
- severity: "high",
1021
- category: "security",
1022
- file: context.filePath,
1023
- startLine: i + 1,
1024
- endLine: i + 1,
1025
- message: t(
1026
- "Potential SQL injection \u2014 string interpolation in SQL query.",
1027
- "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1028
- ),
1029
- suggestion: t(
1030
- "Use parameterized queries or prepared statements instead.",
1031
- "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1032
- )
1033
- });
1034
- }
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
+ });
1035
1043
  }
1036
1044
  }
1037
1045
  return issues;
@@ -2315,12 +2323,7 @@ var noReassignParamRule = {
2315
2323
  }
2316
2324
  }
2317
2325
  if (paramNames.size === 0) return;
2318
- let body = null;
2319
- if (node.type === AST_NODE_TYPES.MethodDefinition) {
2320
- body = node.value;
2321
- } else {
2322
- body = node;
2323
- }
2326
+ const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
2324
2327
  if (!body || !("body" in body)) return;
2325
2328
  const fnBody = body.body;
2326
2329
  if (!fnBody) return;
@@ -2557,15 +2560,33 @@ var RuleEngine = class {
2557
2560
  );
2558
2561
  }
2559
2562
  run(context) {
2563
+ return this.runWithDiagnostics(context).issues;
2564
+ }
2565
+ runWithDiagnostics(context) {
2560
2566
  const allIssues = [];
2567
+ const ruleFailures = [];
2568
+ let rulesExecuted = 0;
2569
+ let rulesFailed = 0;
2561
2570
  for (const rule of this.rules) {
2571
+ rulesExecuted++;
2562
2572
  try {
2563
2573
  const issues = rule.check(context);
2564
2574
  allIssues.push(...issues);
2565
- } 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
+ });
2566
2582
  }
2567
2583
  }
2568
- return allIssues;
2584
+ return {
2585
+ issues: allIssues,
2586
+ rulesExecuted,
2587
+ rulesFailed,
2588
+ ruleFailures
2589
+ };
2569
2590
  }
2570
2591
  getRules() {
2571
2592
  return [...this.rules];
@@ -2945,6 +2966,8 @@ var PKG_VERSION = (() => {
2945
2966
  return "0.1.0";
2946
2967
  }
2947
2968
  })();
2969
+ var REPORT_SCHEMA_VERSION = "1.0.0";
2970
+ var FINGERPRINT_VERSION = "1";
2948
2971
  var ScanEngine = class {
2949
2972
  config;
2950
2973
  diffParser;
@@ -2955,80 +2978,198 @@ var ScanEngine = class {
2955
2978
  this.ruleEngine = new RuleEngine(config);
2956
2979
  }
2957
2980
  async scan(options) {
2958
- const diffFiles = await this.getDiffFiles(options);
2981
+ const selection = await this.getScanCandidates(options);
2959
2982
  const allIssues = [];
2960
- for (const diffFile of diffFiles) {
2961
- if (diffFile.status === "deleted") continue;
2962
- const filePath = resolve2(diffFile.filePath);
2963
- let fileContent;
2964
- try {
2965
- if (existsSync3(filePath)) {
2966
- fileContent = await readFile(filePath, "utf-8");
2967
- } else {
2968
- const content = await this.diffParser.getFileContent(diffFile.filePath);
2969
- if (!content) continue;
2970
- fileContent = content;
2971
- }
2972
- } catch {
2973
- continue;
2974
- }
2975
- const addedLines = diffFile.hunks.flatMap((hunk) => {
2976
- const lines = hunk.content.split("\n");
2977
- const result = [];
2978
- let currentLine = hunk.newStart;
2979
- for (const line of lines) {
2980
- if (line.startsWith("+")) {
2981
- result.push({ lineNumber: currentLine, content: line.slice(1) });
2982
- currentLine++;
2983
- } else if (line.startsWith("-")) {
2984
- } else {
2985
- currentLine++;
2986
- }
2987
- }
2988
- return result;
2989
- });
2990
- const issues = this.ruleEngine.run({
2991
- filePath: diffFile.filePath,
2992
- fileContent,
2993
- addedLines
2994
- });
2995
- allIssues.push(...issues);
2996
- if (this.isTsJsFile(diffFile.filePath)) {
2997
- const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
2998
- maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
2999
- maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3000
- maxFunctionLength: this.config.thresholds["max-function-length"],
3001
- maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3002
- maxParamCount: this.config.thresholds["max-params"]
3003
- });
3004
- allIssues.push(...structureResult.issues);
3005
- const styleResult = analyzeStyle(fileContent, diffFile.filePath);
3006
- allIssues.push(...styleResult.issues);
3007
- const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
3008
- allIssues.push(...coverageResult.issues);
3009
- }
3010
- }
3011
- 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);
3012
3003
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3013
3004
  const grade = getGrade(overallScore);
3014
3005
  const commitHash = await this.diffParser.getCurrentCommitHash();
3015
3006
  return {
3007
+ schemaVersion: REPORT_SCHEMA_VERSION,
3016
3008
  version: PKG_VERSION,
3017
3009
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3018
3010
  commit: commitHash,
3011
+ scanMode: selection.scanMode,
3019
3012
  overall: {
3020
3013
  score: overallScore,
3021
3014
  grade,
3022
- filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
3023
- 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
3024
3027
  },
3025
3028
  dimensions,
3026
- issues: allIssues.sort((a, b) => {
3029
+ issues: issuesWithFingerprints.sort((a, b) => {
3027
3030
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3028
3031
  return severityOrder[a.severity] - severityOrder[b.severity];
3029
3032
  })
3030
3033
  };
3031
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
+ }
3032
3173
  async getDiffFiles(options) {
3033
3174
  if (options.staged) {
3034
3175
  return this.diffParser.getStagedFiles();
@@ -3037,14 +3178,10 @@ var ScanEngine = class {
3037
3178
  return this.diffParser.getDiffFromRef(options.diff);
3038
3179
  }
3039
3180
  if (options.files && options.files.length > 0) {
3181
+ const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
3040
3182
  return Promise.all(
3041
- options.files.map(async (filePath) => {
3042
- let content = "";
3043
- try {
3044
- content = await readFile(resolve2(filePath), "utf-8");
3045
- } catch {
3046
- }
3047
- return {
3183
+ includedFiles.map(
3184
+ (filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
3048
3185
  filePath,
3049
3186
  status: "modified",
3050
3187
  additions: content.split("\n").length,
@@ -3056,18 +3193,64 @@ var ScanEngine = class {
3056
3193
  oldLines: 0,
3057
3194
  newStart: 1,
3058
3195
  newLines: content.split("\n").length,
3059
- content: content.split("\n").map((l) => "+" + l).join("\n")
3196
+ content: content.split("\n").map((line) => "+" + line).join("\n")
3060
3197
  }
3061
3198
  ]
3062
- };
3063
- })
3199
+ }))
3200
+ )
3064
3201
  );
3065
3202
  }
3066
3203
  return this.diffParser.getChangedFiles();
3067
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
+ }
3068
3223
  isTsJsFile(filePath) {
3069
3224
  return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
3070
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
+ }
3071
3254
  groupByDimension(issues) {
3072
3255
  const categories = [
3073
3256
  "security",
@@ -3078,7 +3261,7 @@ var ScanEngine = class {
3078
3261
  ];
3079
3262
  const grouped = {};
3080
3263
  for (const cat of categories) {
3081
- const catIssues = issues.filter((i) => i.category === cat);
3264
+ const catIssues = issues.filter((issue) => issue.category === cat);
3082
3265
  grouped[cat] = calculateDimensionScore(catIssues);
3083
3266
  }
3084
3267
  return grouped;