@gulu9527/code-trust 0.2.1 → 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/index.js CHANGED
@@ -1,38 +1,70 @@
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
8
9
  import simpleGit from "simple-git";
10
+ var GIT_DIFF_UNIFIED = "--unified=3";
11
+ var SHORT_HASH_LENGTH = 7;
9
12
  var DiffParser = class {
10
13
  git;
11
14
  constructor(workDir) {
12
15
  this.git = simpleGit(workDir);
13
16
  }
14
17
  async getStagedFiles() {
15
- const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
18
+ const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
16
19
  return this.parseDiffOutput(diffDetail);
17
20
  }
18
21
  async getDiffFromRef(ref) {
19
- const diffDetail = await this.git.diff([ref, "--unified=3"]);
22
+ const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
20
23
  return this.parseDiffOutput(diffDetail);
21
24
  }
22
25
  async getChangedFiles() {
23
- const diffDetail = await this.git.diff(["--unified=3"]);
24
- const stagedDetail = await this.git.diff(["--cached", "--unified=3"]);
25
- const allDiff = diffDetail + "\n" + stagedDetail;
26
- return this.parseDiffOutput(allDiff);
26
+ const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
27
+ const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
28
+ const unstagedFiles = this.parseDiffOutput(diffDetail);
29
+ const stagedFiles = this.parseDiffOutput(stagedDetail);
30
+ return this.mergeDiffFiles(unstagedFiles, stagedFiles);
31
+ }
32
+ /**
33
+ * Merge two sets of diff files, deduplicating by file path.
34
+ * When a file appears in both, merge their hunks and combine stats.
35
+ */
36
+ mergeDiffFiles(unstaged, staged) {
37
+ const fileMap = /* @__PURE__ */ new Map();
38
+ for (const file of unstaged) {
39
+ fileMap.set(file.filePath, file);
40
+ }
41
+ for (const file of staged) {
42
+ const existing = fileMap.get(file.filePath);
43
+ if (existing) {
44
+ fileMap.set(file.filePath, {
45
+ ...existing,
46
+ // Combine additions/deletions
47
+ additions: existing.additions + file.additions,
48
+ deletions: existing.deletions + file.deletions,
49
+ // Merge hunks (preserve order: staged first, then unstaged)
50
+ hunks: [...file.hunks, ...existing.hunks],
51
+ // Status: if either is 'added', treat as added; otherwise keep modified
52
+ status: existing.status === "added" || file.status === "added" ? "added" : "modified"
53
+ });
54
+ } else {
55
+ fileMap.set(file.filePath, file);
56
+ }
57
+ }
58
+ return Array.from(fileMap.values());
27
59
  }
28
60
  async getLastCommitDiff() {
29
- const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
61
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
30
62
  return this.parseDiffOutput(diffDetail);
31
63
  }
32
64
  async getCurrentCommitHash() {
33
65
  try {
34
66
  const hash = await this.git.revparse(["HEAD"]);
35
- return hash.trim().slice(0, 7);
67
+ return hash.trim().slice(0, SHORT_HASH_LENGTH);
36
68
  } catch {
37
69
  return void 0;
38
70
  }
@@ -468,7 +500,7 @@ function detectCodeAfterReturn(context, lines, issues) {
468
500
  }
469
501
  }
470
502
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
471
- const endsOpen = /[{(\[,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
503
+ const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
472
504
  if (endsOpen) continue;
473
505
  lastReturnDepth = braceDepth;
474
506
  lastReturnLine = i;
@@ -659,6 +691,9 @@ function analyzeFunctionNode(node) {
659
691
  function calculateCyclomaticComplexity(root) {
660
692
  let complexity = 1;
661
693
  walkAST(root, (n) => {
694
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
695
+ return false;
696
+ }
662
697
  switch (n.type) {
663
698
  case AST_NODE_TYPES.IfStatement:
664
699
  case AST_NODE_TYPES.ConditionalExpression:
@@ -687,6 +722,9 @@ function calculateCognitiveComplexity(root) {
687
722
  const depthMap = /* @__PURE__ */ new WeakMap();
688
723
  depthMap.set(root, 0);
689
724
  walkAST(root, (n, parent) => {
725
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
726
+ return false;
727
+ }
690
728
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
691
729
  const isNesting = isNestingNode(n);
692
730
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -705,6 +743,9 @@ function calculateMaxNestingDepth(root) {
705
743
  const depthMap = /* @__PURE__ */ new WeakMap();
706
744
  depthMap.set(root, 0);
707
745
  walkAST(root, (n, parent) => {
746
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
747
+ return false;
748
+ }
708
749
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
709
750
  const isNesting = n.type === AST_NODE_TYPES.IfStatement || n.type === AST_NODE_TYPES.ForStatement || n.type === AST_NODE_TYPES.ForInStatement || n.type === AST_NODE_TYPES.ForOfStatement || n.type === AST_NODE_TYPES.WhileStatement || n.type === AST_NODE_TYPES.DoWhileStatement || n.type === AST_NODE_TYPES.SwitchStatement || n.type === AST_NODE_TYPES.TryStatement;
710
751
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -886,14 +927,19 @@ function stringifyCondition(node) {
886
927
  case AST_NODE_TYPES.Literal:
887
928
  return String(node.value);
888
929
  case AST_NODE_TYPES.BinaryExpression:
930
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
889
931
  case AST_NODE_TYPES.LogicalExpression:
890
932
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
891
933
  case AST_NODE_TYPES.UnaryExpression:
892
934
  return `${node.operator}${stringifyCondition(node.argument)}`;
893
935
  case AST_NODE_TYPES.MemberExpression:
894
936
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
895
- case AST_NODE_TYPES.CallExpression:
896
- return `${stringifyCondition(node.callee)}(...)`;
937
+ case AST_NODE_TYPES.CallExpression: {
938
+ const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
939
+ return `${stringifyCondition(node.callee)}(${args})`;
940
+ }
941
+ case AST_NODE_TYPES.ConditionalExpression:
942
+ return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
897
943
  default:
898
944
  return `[${node.type}]`;
899
945
  }
@@ -903,6 +949,10 @@ function truncate(s, maxLen) {
903
949
  }
904
950
 
905
951
  // src/rules/builtin/security.ts
952
+ var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
953
+ var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
954
+ var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
955
+ var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
906
956
  var securityRules = [
907
957
  {
908
958
  id: "security/hardcoded-secret",
@@ -915,7 +965,7 @@ var securityRules = [
915
965
  const lines = context.fileContent.split("\n");
916
966
  const secretPatterns = [
917
967
  // API keys / tokens
918
- { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_\-]{16,}['"`]/i, label: "API key" },
968
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
919
969
  { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
920
970
  // AWS
921
971
  { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
@@ -968,15 +1018,17 @@ var securityRules = [
968
1018
  const issues = [];
969
1019
  const lines = context.fileContent.split("\n");
970
1020
  for (let i = 0; i < lines.length; i++) {
971
- const trimmed = lines[i].trim();
972
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1021
+ const line = lines[i];
1022
+ const trimmed = line.trim();
1023
+ if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
1024
+ const sanitizedLine = stripQuotedStrings(line);
973
1025
  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" }
1026
+ { pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
1027
+ { pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
1028
+ { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
977
1029
  ];
978
- for (const { pattern, label } of evalPatterns) {
979
- if (pattern.test(lines[i])) {
1030
+ for (const { pattern, label, source } of evalPatterns) {
1031
+ if (pattern.test(source)) {
980
1032
  issues.push({
981
1033
  ruleId: "security/eval-usage",
982
1034
  severity: "high",
@@ -1009,29 +1061,30 @@ var securityRules = [
1009
1061
  check(context) {
1010
1062
  const issues = [];
1011
1063
  const lines = context.fileContent.split("\n");
1064
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1012
1065
  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
- }
1066
+ const line = lines[i];
1067
+ const trimmed = line.trim();
1068
+ if (isCommentLine(trimmed)) continue;
1069
+ const hasSqlKeyword = sqlKeywords.test(line);
1070
+ const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
1071
+ if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
1072
+ issues.push({
1073
+ ruleId: "security/sql-injection",
1074
+ severity: "high",
1075
+ category: "security",
1076
+ file: context.filePath,
1077
+ startLine: i + 1,
1078
+ endLine: i + 1,
1079
+ message: t(
1080
+ "Potential SQL injection \u2014 string interpolation in SQL query.",
1081
+ "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1082
+ ),
1083
+ suggestion: t(
1084
+ "Use parameterized queries or prepared statements instead.",
1085
+ "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1086
+ )
1087
+ });
1035
1088
  }
1036
1089
  }
1037
1090
  return issues;
@@ -1047,9 +1100,11 @@ var securityRules = [
1047
1100
  const issues = [];
1048
1101
  const lines = context.fileContent.split("\n");
1049
1102
  for (let i = 0; i < lines.length; i++) {
1050
- const trimmed = lines[i].trim();
1103
+ const line = lines[i];
1104
+ const trimmed = line.trim();
1051
1105
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1052
- if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1106
+ const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
1107
+ if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
1053
1108
  issues.push({
1054
1109
  ruleId: "security/dangerous-html",
1055
1110
  severity: "medium",
@@ -1577,6 +1632,9 @@ var missingAwaitRule = {
1577
1632
  const body = getFunctionBody(node);
1578
1633
  if (!body) return;
1579
1634
  walkAST(body, (inner, parent) => {
1635
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1636
+ return;
1637
+ }
1580
1638
  if (inner !== body && isAsyncFunction(inner)) return false;
1581
1639
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1582
1640
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1586,6 +1644,9 @@ var missingAwaitRule = {
1586
1644
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1587
1645
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1588
1646
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1647
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1648
+ return;
1649
+ }
1589
1650
  const callName = getCallName(inner);
1590
1651
  if (!callName) return;
1591
1652
  if (!asyncFuncNames.has(callName)) return;
@@ -1797,9 +1858,28 @@ var typeCoercionRule = {
1797
1858
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1798
1859
  -1,
1799
1860
  0,
1861
+ 0.1,
1862
+ 0.1,
1863
+ 0.15,
1864
+ 0.2,
1865
+ 0.2,
1866
+ 0.25,
1867
+ 0.3,
1868
+ 0.3,
1869
+ 0.5,
1800
1870
  1,
1801
1871
  2,
1872
+ 3,
1873
+ 4,
1874
+ 5,
1802
1875
  10,
1876
+ 15,
1877
+ 20,
1878
+ 30,
1879
+ 40,
1880
+ 50,
1881
+ 70,
1882
+ 90,
1803
1883
  100
1804
1884
  ]);
1805
1885
  var magicNumberRule = {
@@ -1828,6 +1908,7 @@ var magicNumberRule = {
1828
1908
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1829
1909
  if (trimmed.startsWith("import ")) continue;
1830
1910
  if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
1911
+ if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
1831
1912
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1832
1913
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1833
1914
  let match;
@@ -1924,6 +2005,17 @@ var nestedTernaryRule = {
1924
2005
  // src/rules/builtin/duplicate-string.ts
1925
2006
  var MIN_STRING_LENGTH = 6;
1926
2007
  var MIN_OCCURRENCES = 3;
2008
+ var IGNORED_LITERALS = /* @__PURE__ */ new Set([
2009
+ "high",
2010
+ "medium",
2011
+ "low",
2012
+ "info",
2013
+ "logic",
2014
+ "security",
2015
+ "structure",
2016
+ "style",
2017
+ "coverage"
2018
+ ]);
1927
2019
  var duplicateStringRule = {
1928
2020
  id: "logic/duplicate-string",
1929
2021
  category: "logic",
@@ -1954,6 +2046,7 @@ var duplicateStringRule = {
1954
2046
  while ((match = stringRegex.exec(cleaned)) !== null) {
1955
2047
  const value = match[2];
1956
2048
  if (value.length < MIN_STRING_LENGTH) continue;
2049
+ if (IGNORED_LITERALS.has(value)) continue;
1957
2050
  if (value.includes("${")) continue;
1958
2051
  if (value.startsWith("http") || value.startsWith("/")) continue;
1959
2052
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2223,13 +2316,28 @@ var promiseVoidRule = {
2223
2316
  /^save/,
2224
2317
  /^load/,
2225
2318
  /^send/,
2226
- /^delete/,
2227
2319
  /^update/,
2228
2320
  /^create/,
2229
2321
  /^connect/,
2230
2322
  /^disconnect/,
2231
2323
  /^init/
2232
2324
  ];
2325
+ const syncMethods = [
2326
+ "delete",
2327
+ // Map.delete(), Set.delete(), Object.delete() are synchronous
2328
+ "has",
2329
+ // Map.has(), Set.has() are synchronous
2330
+ "get",
2331
+ // Map.get() is synchronous
2332
+ "set",
2333
+ // Map.set() is synchronous (though some consider it potentially async)
2334
+ "keys",
2335
+ // Object.keys() is synchronous
2336
+ "values",
2337
+ // Object.values() is synchronous
2338
+ "entries"
2339
+ // Object.entries() is synchronous
2340
+ ];
2233
2341
  walkAST(ast, (node) => {
2234
2342
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2235
2343
  const expr = node.expression;
@@ -2240,6 +2348,7 @@ var promiseVoidRule = {
2240
2348
  const isKnownAsync = asyncFnNames.has(fnName);
2241
2349
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2242
2350
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2351
+ if (syncMethods.includes(fnName)) return;
2243
2352
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2244
2353
  const line = node.loc?.start.line ?? 0;
2245
2354
  if (line === 0) return;
@@ -2315,12 +2424,7 @@ var noReassignParamRule = {
2315
2424
  }
2316
2425
  }
2317
2426
  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
- }
2427
+ const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
2324
2428
  if (!body || !("body" in body)) return;
2325
2429
  const fnBody = body.body;
2326
2430
  if (!fnBody) return;
@@ -2557,15 +2661,33 @@ var RuleEngine = class {
2557
2661
  );
2558
2662
  }
2559
2663
  run(context) {
2664
+ return this.runWithDiagnostics(context).issues;
2665
+ }
2666
+ runWithDiagnostics(context) {
2560
2667
  const allIssues = [];
2668
+ const ruleFailures = [];
2669
+ let rulesExecuted = 0;
2670
+ let rulesFailed = 0;
2561
2671
  for (const rule of this.rules) {
2672
+ rulesExecuted++;
2562
2673
  try {
2563
2674
  const issues = rule.check(context);
2564
2675
  allIssues.push(...issues);
2565
- } catch (_err) {
2676
+ } catch (err) {
2677
+ rulesFailed++;
2678
+ ruleFailures.push({
2679
+ ruleId: rule.id,
2680
+ file: context.filePath,
2681
+ message: err instanceof Error ? err.message : "Unknown rule execution failure"
2682
+ });
2566
2683
  }
2567
2684
  }
2568
- return allIssues;
2685
+ return {
2686
+ issues: allIssues,
2687
+ rulesExecuted,
2688
+ rulesFailed,
2689
+ ruleFailures
2690
+ };
2569
2691
  }
2570
2692
  getRules() {
2571
2693
  return [...this.rules];
@@ -2945,6 +3067,8 @@ var PKG_VERSION = (() => {
2945
3067
  return "0.1.0";
2946
3068
  }
2947
3069
  })();
3070
+ var REPORT_SCHEMA_VERSION = "1.0.0";
3071
+ var FINGERPRINT_VERSION = "1";
2948
3072
  var ScanEngine = class {
2949
3073
  config;
2950
3074
  diffParser;
@@ -2955,81 +3079,191 @@ var ScanEngine = class {
2955
3079
  this.ruleEngine = new RuleEngine(config);
2956
3080
  }
2957
3081
  async scan(options) {
2958
- const diffFiles = await this.getDiffFiles(options);
3082
+ const selection = await this.getScanCandidates(options);
2959
3083
  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);
3084
+ const scanErrors = [];
3085
+ const ruleFailures = [];
3086
+ let rulesExecuted = 0;
3087
+ let rulesFailed = 0;
3088
+ let filesScanned = 0;
3089
+ const results = await Promise.all(
3090
+ selection.candidates.map((diffFile) => this.scanFile(diffFile))
3091
+ );
3092
+ for (const result of results) {
3093
+ allIssues.push(...result.issues);
3094
+ ruleFailures.push(...result.ruleFailures);
3095
+ scanErrors.push(...result.scanErrors);
3096
+ rulesExecuted += result.rulesExecuted;
3097
+ rulesFailed += result.rulesFailed;
3098
+ if (result.scanned) {
3099
+ filesScanned++;
3100
+ }
3101
+ }
3102
+ const issuesWithFingerprints = this.attachFingerprints(allIssues);
3103
+ const baseline = await this.loadBaseline(options.baseline);
3104
+ const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
3105
+ const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
3106
+ const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
3107
+ const dimensions = this.groupByDimension(issuesWithLifecycle);
3012
3108
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3013
3109
  const grade = getGrade(overallScore);
3014
3110
  const commitHash = await this.diffParser.getCurrentCommitHash();
3015
3111
  return {
3112
+ schemaVersion: REPORT_SCHEMA_VERSION,
3016
3113
  version: PKG_VERSION,
3017
3114
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3018
3115
  commit: commitHash,
3116
+ scanMode: selection.scanMode,
3019
3117
  overall: {
3020
3118
  score: overallScore,
3021
3119
  grade,
3022
- filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
3023
- issuesFound: allIssues.length
3120
+ filesScanned,
3121
+ issuesFound: issuesWithLifecycle.length
3122
+ },
3123
+ toolHealth: {
3124
+ rulesExecuted,
3125
+ rulesFailed,
3126
+ filesConsidered: selection.filesConsidered,
3127
+ filesScanned,
3128
+ filesExcluded: selection.filesExcluded,
3129
+ filesSkipped: scanErrors.length,
3130
+ scanErrors,
3131
+ ruleFailures
3024
3132
  },
3025
3133
  dimensions,
3026
- issues: allIssues.sort((a, b) => {
3134
+ issues: issuesWithLifecycle.sort((a, b) => {
3027
3135
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3028
3136
  return severityOrder[a.severity] - severityOrder[b.severity];
3029
- })
3137
+ }),
3138
+ lifecycle,
3139
+ fixedIssues
3030
3140
  };
3031
3141
  }
3032
- async getDiffFiles(options) {
3142
+ async scanFile(diffFile) {
3143
+ if (diffFile.status === "deleted") {
3144
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3145
+ }
3146
+ if (!this.isTsJsFile(diffFile.filePath)) {
3147
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
3148
+ }
3149
+ const fileContent = await this.readFileContent(diffFile);
3150
+ if (!fileContent) {
3151
+ return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
3152
+ }
3153
+ const addedLines = this.extractAddedLines(diffFile);
3154
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3155
+ filePath: diffFile.filePath,
3156
+ fileContent,
3157
+ addedLines
3158
+ });
3159
+ const issues = [...ruleResult.issues];
3160
+ issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
3161
+ issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
3162
+ issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
3163
+ return {
3164
+ issues,
3165
+ ruleFailures: ruleResult.ruleFailures,
3166
+ rulesExecuted: ruleResult.rulesExecuted,
3167
+ rulesFailed: ruleResult.rulesFailed,
3168
+ scanErrors: [],
3169
+ scanned: true
3170
+ };
3171
+ }
3172
+ createSkippedResult(diffFile, type, message) {
3173
+ return {
3174
+ issues: [],
3175
+ ruleFailures: [],
3176
+ rulesExecuted: 0,
3177
+ rulesFailed: 0,
3178
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3179
+ scanned: false
3180
+ };
3181
+ }
3182
+ createErrorResult(diffFile, type, message) {
3183
+ return {
3184
+ issues: [],
3185
+ ruleFailures: [],
3186
+ rulesExecuted: 0,
3187
+ rulesFailed: 0,
3188
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3189
+ scanned: false
3190
+ };
3191
+ }
3192
+ async readFileContent(diffFile) {
3193
+ const filePath = resolve2(diffFile.filePath);
3194
+ try {
3195
+ return await readFile(filePath, "utf-8");
3196
+ } catch {
3197
+ const content = await this.diffParser.getFileContent(diffFile.filePath);
3198
+ return content ?? null;
3199
+ }
3200
+ }
3201
+ extractAddedLines(diffFile) {
3202
+ return diffFile.hunks.flatMap((hunk) => {
3203
+ const lines = hunk.content.split("\n");
3204
+ const result = [];
3205
+ let currentLine = hunk.newStart;
3206
+ for (const line of lines) {
3207
+ if (line.startsWith("+")) {
3208
+ result.push({ lineNumber: currentLine, content: line.slice(1) });
3209
+ currentLine++;
3210
+ } else if (line.startsWith("-")) {
3211
+ } else {
3212
+ currentLine++;
3213
+ }
3214
+ }
3215
+ return result;
3216
+ });
3217
+ }
3218
+ runStructureAnalysis(fileContent, filePath) {
3219
+ return analyzeStructure(fileContent, filePath, {
3220
+ maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3221
+ maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3222
+ maxFunctionLength: this.config.thresholds["max-function-length"],
3223
+ maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3224
+ maxParamCount: this.config.thresholds["max-params"]
3225
+ }).issues;
3226
+ }
3227
+ async getScanCandidates(options) {
3228
+ const scanMode = this.getScanMode(options);
3229
+ const candidates = await this.getDiffFiles(options);
3230
+ if (scanMode === "files") {
3231
+ return {
3232
+ scanMode,
3233
+ candidates,
3234
+ filesConsidered: options.files?.length ?? candidates.length,
3235
+ filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
3236
+ };
3237
+ }
3238
+ const filteredCandidates = [];
3239
+ let filesExcluded = 0;
3240
+ for (const candidate of candidates) {
3241
+ if (this.shouldIncludeFile(candidate.filePath)) {
3242
+ filteredCandidates.push(candidate);
3243
+ } else {
3244
+ filesExcluded++;
3245
+ }
3246
+ }
3247
+ return {
3248
+ scanMode,
3249
+ candidates: filteredCandidates,
3250
+ filesConsidered: candidates.length,
3251
+ filesExcluded
3252
+ };
3253
+ }
3254
+ getScanMode(options) {
3255
+ if (options.staged) {
3256
+ return "staged";
3257
+ }
3258
+ if (options.diff) {
3259
+ return "diff";
3260
+ }
3261
+ if (options.files && options.files.length > 0) {
3262
+ return "files";
3263
+ }
3264
+ return "changed";
3265
+ }
3266
+ getDiffFiles(options) {
3033
3267
  if (options.staged) {
3034
3268
  return this.diffParser.getStagedFiles();
3035
3269
  }
@@ -3037,14 +3271,10 @@ var ScanEngine = class {
3037
3271
  return this.diffParser.getDiffFromRef(options.diff);
3038
3272
  }
3039
3273
  if (options.files && options.files.length > 0) {
3274
+ const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
3040
3275
  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 {
3276
+ includedFiles.map(
3277
+ (filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
3048
3278
  filePath,
3049
3279
  status: "modified",
3050
3280
  additions: content.split("\n").length,
@@ -3056,18 +3286,154 @@ var ScanEngine = class {
3056
3286
  oldLines: 0,
3057
3287
  newStart: 1,
3058
3288
  newLines: content.split("\n").length,
3059
- content: content.split("\n").map((l) => "+" + l).join("\n")
3289
+ content: content.split("\n").map((line) => "+" + line).join("\n")
3060
3290
  }
3061
3291
  ]
3062
- };
3063
- })
3292
+ }))
3293
+ )
3064
3294
  );
3065
3295
  }
3066
3296
  return this.diffParser.getChangedFiles();
3067
3297
  }
3298
+ shouldIncludeFile(filePath) {
3299
+ const normalizedPath = filePath.split(sep).join("/");
3300
+ const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
3301
+ const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3302
+ if (!included) {
3303
+ return false;
3304
+ }
3305
+ return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3306
+ }
3307
+ matchesPattern(filePath, pattern) {
3308
+ let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3309
+ regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
3310
+ regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
3311
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*");
3312
+ regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
3313
+ regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
3314
+ return new RegExp(`^${regexPattern}$`).test(filePath);
3315
+ }
3068
3316
  isTsJsFile(filePath) {
3069
3317
  return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
3070
3318
  }
3319
+ attachFingerprints(issues) {
3320
+ const occurrenceCounts = /* @__PURE__ */ new Map();
3321
+ return issues.map((issue) => {
3322
+ const normalizedFile = this.normalizeRelativePath(issue.file);
3323
+ const locationComponent = `${issue.startLine}:${issue.endLine}`;
3324
+ const baseKey = [
3325
+ issue.ruleId,
3326
+ normalizedFile,
3327
+ issue.category,
3328
+ issue.severity,
3329
+ locationComponent
3330
+ ].join("|");
3331
+ const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3332
+ occurrenceCounts.set(baseKey, occurrenceIndex + 1);
3333
+ const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
3334
+ return {
3335
+ ...issue,
3336
+ file: normalizedFile,
3337
+ fingerprint,
3338
+ fingerprintVersion: FINGERPRINT_VERSION
3339
+ };
3340
+ });
3341
+ }
3342
+ normalizeRelativePath(filePath) {
3343
+ const absolutePath = resolve2(filePath);
3344
+ const relativePath = relative(process.cwd(), absolutePath) || filePath;
3345
+ return relativePath.split(sep).join("/");
3346
+ }
3347
+ async loadBaseline(baselinePath) {
3348
+ if (!baselinePath) {
3349
+ return void 0;
3350
+ }
3351
+ const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
3352
+ const parsed = JSON.parse(baselineContent);
3353
+ const issues = this.parseBaselineIssues(parsed.issues);
3354
+ return {
3355
+ issues,
3356
+ fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
3357
+ commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
3358
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
3359
+ };
3360
+ }
3361
+ parseBaselineIssues(input) {
3362
+ if (!Array.isArray(input)) {
3363
+ return [];
3364
+ }
3365
+ return input.flatMap((item) => {
3366
+ const issue = this.parseBaselineIssue(item);
3367
+ return issue ? [issue] : [];
3368
+ });
3369
+ }
3370
+ parseBaselineIssue(input) {
3371
+ if (!input || typeof input !== "object") {
3372
+ return void 0;
3373
+ }
3374
+ const issue = input;
3375
+ if (!this.isValidBaselineIssue(issue)) {
3376
+ return void 0;
3377
+ }
3378
+ return {
3379
+ ruleId: issue.ruleId,
3380
+ severity: issue.severity,
3381
+ category: issue.category,
3382
+ file: issue.file,
3383
+ startLine: issue.startLine,
3384
+ endLine: issue.endLine,
3385
+ message: issue.message,
3386
+ fingerprint: issue.fingerprint,
3387
+ fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
3388
+ };
3389
+ }
3390
+ isValidBaselineIssue(issue) {
3391
+ return typeof issue.ruleId === "string" && this.isSeverity(issue.severity) && this.isRuleCategory(issue.category) && typeof issue.file === "string" && typeof issue.startLine === "number" && typeof issue.endLine === "number" && typeof issue.message === "string" && typeof issue.fingerprint === "string";
3392
+ }
3393
+ attachLifecycle(issues, baseline) {
3394
+ if (!baseline) {
3395
+ return issues;
3396
+ }
3397
+ return issues.map((issue) => ({
3398
+ ...issue,
3399
+ lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
3400
+ }));
3401
+ }
3402
+ getFixedIssues(issues, baseline) {
3403
+ if (!baseline) {
3404
+ return [];
3405
+ }
3406
+ const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
3407
+ return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
3408
+ }
3409
+ buildLifecycleSummary(issues, fixedIssues, baseline) {
3410
+ if (!baseline) {
3411
+ return void 0;
3412
+ }
3413
+ let newIssues = 0;
3414
+ let existingIssues = 0;
3415
+ for (const issue of issues) {
3416
+ if (issue.lifecycle === "existing") {
3417
+ existingIssues++;
3418
+ } else {
3419
+ newIssues++;
3420
+ }
3421
+ }
3422
+ return {
3423
+ newIssues,
3424
+ existingIssues,
3425
+ fixedIssues: fixedIssues.length,
3426
+ baselineUsed: true,
3427
+ baselineCommit: baseline.commit,
3428
+ baselineTimestamp: baseline.timestamp
3429
+ };
3430
+ }
3431
+ isSeverity(value) {
3432
+ return value === "high" || value === "medium" || value === "low" || value === "info";
3433
+ }
3434
+ isRuleCategory(value) {
3435
+ return ["security", "logic", "structure", "style", "coverage"].includes(value);
3436
+ }
3071
3437
  groupByDimension(issues) {
3072
3438
  const categories = [
3073
3439
  "security",
@@ -3078,7 +3444,7 @@ var ScanEngine = class {
3078
3444
  ];
3079
3445
  const grouped = {};
3080
3446
  for (const cat of categories) {
3081
- const catIssues = issues.filter((i) => i.category === cat);
3447
+ const catIssues = issues.filter((issue) => issue.category === cat);
3082
3448
  grouped[cat] = calculateDimensionScore(catIssues);
3083
3449
  }
3084
3450
  return grouped;