@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/README-CN.md +256 -0
- package/README.md +42 -4
- package/dist/cli/index.js +548 -138
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +89 -2
- package/dist/index.js +489 -123
- package/dist/index.js.map +1 -1
- package/docs/codetrust-deep-research-report-zh-en.md +802 -0
- package/package.json +6 -2
package/dist/cli/index.js
CHANGED
|
@@ -120,40 +120,72 @@ detection:
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// src/core/engine.ts
|
|
123
|
+
import { createHash } from "crypto";
|
|
123
124
|
import { readFile } from "fs/promises";
|
|
124
|
-
import { readFileSync
|
|
125
|
-
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
125
|
+
import { readFileSync } from "fs";
|
|
126
|
+
import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
|
|
126
127
|
import { fileURLToPath } from "url";
|
|
127
128
|
|
|
128
129
|
// src/parsers/diff.ts
|
|
129
130
|
import simpleGit from "simple-git";
|
|
131
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
132
|
+
var SHORT_HASH_LENGTH = 7;
|
|
130
133
|
var DiffParser = class {
|
|
131
134
|
git;
|
|
132
135
|
constructor(workDir) {
|
|
133
136
|
this.git = simpleGit(workDir);
|
|
134
137
|
}
|
|
135
138
|
async getStagedFiles() {
|
|
136
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
139
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
137
140
|
return this.parseDiffOutput(diffDetail);
|
|
138
141
|
}
|
|
139
142
|
async getDiffFromRef(ref) {
|
|
140
|
-
const diffDetail = await this.git.diff([ref,
|
|
143
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
141
144
|
return this.parseDiffOutput(diffDetail);
|
|
142
145
|
}
|
|
143
146
|
async getChangedFiles() {
|
|
144
|
-
const diffDetail = await this.git.diff([
|
|
145
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
146
|
-
const
|
|
147
|
-
|
|
147
|
+
const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
|
|
148
|
+
const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
149
|
+
const unstagedFiles = this.parseDiffOutput(diffDetail);
|
|
150
|
+
const stagedFiles = this.parseDiffOutput(stagedDetail);
|
|
151
|
+
return this.mergeDiffFiles(unstagedFiles, stagedFiles);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Merge two sets of diff files, deduplicating by file path.
|
|
155
|
+
* When a file appears in both, merge their hunks and combine stats.
|
|
156
|
+
*/
|
|
157
|
+
mergeDiffFiles(unstaged, staged) {
|
|
158
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const file of unstaged) {
|
|
160
|
+
fileMap.set(file.filePath, file);
|
|
161
|
+
}
|
|
162
|
+
for (const file of staged) {
|
|
163
|
+
const existing = fileMap.get(file.filePath);
|
|
164
|
+
if (existing) {
|
|
165
|
+
fileMap.set(file.filePath, {
|
|
166
|
+
...existing,
|
|
167
|
+
// Combine additions/deletions
|
|
168
|
+
additions: existing.additions + file.additions,
|
|
169
|
+
deletions: existing.deletions + file.deletions,
|
|
170
|
+
// Merge hunks (preserve order: staged first, then unstaged)
|
|
171
|
+
hunks: [...file.hunks, ...existing.hunks],
|
|
172
|
+
// Status: if either is 'added', treat as added; otherwise keep modified
|
|
173
|
+
status: existing.status === "added" || file.status === "added" ? "added" : "modified"
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
fileMap.set(file.filePath, file);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Array.from(fileMap.values());
|
|
148
180
|
}
|
|
149
181
|
async getLastCommitDiff() {
|
|
150
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
182
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
151
183
|
return this.parseDiffOutput(diffDetail);
|
|
152
184
|
}
|
|
153
185
|
async getCurrentCommitHash() {
|
|
154
186
|
try {
|
|
155
187
|
const hash = await this.git.revparse(["HEAD"]);
|
|
156
|
-
return hash.trim().slice(0,
|
|
188
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
157
189
|
} catch {
|
|
158
190
|
return void 0;
|
|
159
191
|
}
|
|
@@ -589,7 +621,7 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
589
621
|
}
|
|
590
622
|
}
|
|
591
623
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
592
|
-
const endsOpen = /[{(
|
|
624
|
+
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
593
625
|
if (endsOpen) continue;
|
|
594
626
|
lastReturnDepth = braceDepth;
|
|
595
627
|
lastReturnLine = i;
|
|
@@ -780,6 +812,9 @@ function analyzeFunctionNode(node) {
|
|
|
780
812
|
function calculateCyclomaticComplexity(root) {
|
|
781
813
|
let complexity = 1;
|
|
782
814
|
walkAST(root, (n) => {
|
|
815
|
+
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) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
783
818
|
switch (n.type) {
|
|
784
819
|
case AST_NODE_TYPES.IfStatement:
|
|
785
820
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -808,6 +843,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
808
843
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
809
844
|
depthMap.set(root, 0);
|
|
810
845
|
walkAST(root, (n, parent) => {
|
|
846
|
+
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) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
811
849
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
812
850
|
const isNesting = isNestingNode(n);
|
|
813
851
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -826,6 +864,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
826
864
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
827
865
|
depthMap.set(root, 0);
|
|
828
866
|
walkAST(root, (n, parent) => {
|
|
867
|
+
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) {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
829
870
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
830
871
|
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;
|
|
831
872
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -1007,14 +1048,19 @@ function stringifyCondition(node) {
|
|
|
1007
1048
|
case AST_NODE_TYPES.Literal:
|
|
1008
1049
|
return String(node.value);
|
|
1009
1050
|
case AST_NODE_TYPES.BinaryExpression:
|
|
1051
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1010
1052
|
case AST_NODE_TYPES.LogicalExpression:
|
|
1011
1053
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1012
1054
|
case AST_NODE_TYPES.UnaryExpression:
|
|
1013
1055
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
1014
1056
|
case AST_NODE_TYPES.MemberExpression:
|
|
1015
1057
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
1016
|
-
case AST_NODE_TYPES.CallExpression:
|
|
1017
|
-
|
|
1058
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
1059
|
+
const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
|
|
1060
|
+
return `${stringifyCondition(node.callee)}(${args})`;
|
|
1061
|
+
}
|
|
1062
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
1063
|
+
return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
|
|
1018
1064
|
default:
|
|
1019
1065
|
return `[${node.type}]`;
|
|
1020
1066
|
}
|
|
@@ -1024,6 +1070,10 @@ function truncate(s, maxLen) {
|
|
|
1024
1070
|
}
|
|
1025
1071
|
|
|
1026
1072
|
// src/rules/builtin/security.ts
|
|
1073
|
+
var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
|
|
1074
|
+
var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
|
|
1075
|
+
var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
|
|
1076
|
+
var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
|
|
1027
1077
|
var securityRules = [
|
|
1028
1078
|
{
|
|
1029
1079
|
id: "security/hardcoded-secret",
|
|
@@ -1036,7 +1086,7 @@ var securityRules = [
|
|
|
1036
1086
|
const lines = context.fileContent.split("\n");
|
|
1037
1087
|
const secretPatterns = [
|
|
1038
1088
|
// API keys / tokens
|
|
1039
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_
|
|
1089
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
|
|
1040
1090
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
1041
1091
|
// AWS
|
|
1042
1092
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -1089,15 +1139,17 @@ var securityRules = [
|
|
|
1089
1139
|
const issues = [];
|
|
1090
1140
|
const lines = context.fileContent.split("\n");
|
|
1091
1141
|
for (let i = 0; i < lines.length; i++) {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1142
|
+
const line = lines[i];
|
|
1143
|
+
const trimmed = line.trim();
|
|
1144
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
1145
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
1094
1146
|
const evalPatterns = [
|
|
1095
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
1096
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
1097
|
-
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
|
|
1147
|
+
{ pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
|
|
1148
|
+
{ pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
|
|
1149
|
+
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
|
|
1098
1150
|
];
|
|
1099
|
-
for (const { pattern, label } of evalPatterns) {
|
|
1100
|
-
if (pattern.test(
|
|
1151
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
1152
|
+
if (pattern.test(source)) {
|
|
1101
1153
|
issues.push({
|
|
1102
1154
|
ruleId: "security/eval-usage",
|
|
1103
1155
|
severity: "high",
|
|
@@ -1130,29 +1182,30 @@ var securityRules = [
|
|
|
1130
1182
|
check(context) {
|
|
1131
1183
|
const issues = [];
|
|
1132
1184
|
const lines = context.fileContent.split("\n");
|
|
1185
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
1133
1186
|
for (let i = 0; i < lines.length; i++) {
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1187
|
+
const line = lines[i];
|
|
1188
|
+
const trimmed = line.trim();
|
|
1189
|
+
if (isCommentLine(trimmed)) continue;
|
|
1190
|
+
const hasSqlKeyword = sqlKeywords.test(line);
|
|
1191
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
|
|
1192
|
+
if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
|
|
1193
|
+
issues.push({
|
|
1194
|
+
ruleId: "security/sql-injection",
|
|
1195
|
+
severity: "high",
|
|
1196
|
+
category: "security",
|
|
1197
|
+
file: context.filePath,
|
|
1198
|
+
startLine: i + 1,
|
|
1199
|
+
endLine: i + 1,
|
|
1200
|
+
message: t(
|
|
1201
|
+
"Potential SQL injection \u2014 string interpolation in SQL query.",
|
|
1202
|
+
"\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
|
|
1203
|
+
),
|
|
1204
|
+
suggestion: t(
|
|
1205
|
+
"Use parameterized queries or prepared statements instead.",
|
|
1206
|
+
"\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
|
|
1207
|
+
)
|
|
1208
|
+
});
|
|
1156
1209
|
}
|
|
1157
1210
|
}
|
|
1158
1211
|
return issues;
|
|
@@ -1168,9 +1221,11 @@ var securityRules = [
|
|
|
1168
1221
|
const issues = [];
|
|
1169
1222
|
const lines = context.fileContent.split("\n");
|
|
1170
1223
|
for (let i = 0; i < lines.length; i++) {
|
|
1171
|
-
const
|
|
1224
|
+
const line = lines[i];
|
|
1225
|
+
const trimmed = line.trim();
|
|
1172
1226
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1173
|
-
|
|
1227
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1228
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1174
1229
|
issues.push({
|
|
1175
1230
|
ruleId: "security/dangerous-html",
|
|
1176
1231
|
severity: "medium",
|
|
@@ -1698,6 +1753,9 @@ var missingAwaitRule = {
|
|
|
1698
1753
|
const body = getFunctionBody(node);
|
|
1699
1754
|
if (!body) return;
|
|
1700
1755
|
walkAST(body, (inner, parent) => {
|
|
1756
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1701
1759
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1702
1760
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1703
1761
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1707,6 +1765,9 @@ var missingAwaitRule = {
|
|
|
1707
1765
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1708
1766
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1709
1767
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1768
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1710
1771
|
const callName = getCallName(inner);
|
|
1711
1772
|
if (!callName) return;
|
|
1712
1773
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1918,9 +1979,28 @@ var typeCoercionRule = {
|
|
|
1918
1979
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1919
1980
|
-1,
|
|
1920
1981
|
0,
|
|
1982
|
+
0.1,
|
|
1983
|
+
0.1,
|
|
1984
|
+
0.15,
|
|
1985
|
+
0.2,
|
|
1986
|
+
0.2,
|
|
1987
|
+
0.25,
|
|
1988
|
+
0.3,
|
|
1989
|
+
0.3,
|
|
1990
|
+
0.5,
|
|
1921
1991
|
1,
|
|
1922
1992
|
2,
|
|
1993
|
+
3,
|
|
1994
|
+
4,
|
|
1995
|
+
5,
|
|
1923
1996
|
10,
|
|
1997
|
+
15,
|
|
1998
|
+
20,
|
|
1999
|
+
30,
|
|
2000
|
+
40,
|
|
2001
|
+
50,
|
|
2002
|
+
70,
|
|
2003
|
+
90,
|
|
1924
2004
|
100
|
|
1925
2005
|
]);
|
|
1926
2006
|
var magicNumberRule = {
|
|
@@ -1949,6 +2029,7 @@ var magicNumberRule = {
|
|
|
1949
2029
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1950
2030
|
if (trimmed.startsWith("import ")) continue;
|
|
1951
2031
|
if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
|
|
2032
|
+
if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
|
|
1952
2033
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1953
2034
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1954
2035
|
let match;
|
|
@@ -2045,6 +2126,17 @@ var nestedTernaryRule = {
|
|
|
2045
2126
|
// src/rules/builtin/duplicate-string.ts
|
|
2046
2127
|
var MIN_STRING_LENGTH = 6;
|
|
2047
2128
|
var MIN_OCCURRENCES = 3;
|
|
2129
|
+
var IGNORED_LITERALS = /* @__PURE__ */ new Set([
|
|
2130
|
+
"high",
|
|
2131
|
+
"medium",
|
|
2132
|
+
"low",
|
|
2133
|
+
"info",
|
|
2134
|
+
"logic",
|
|
2135
|
+
"security",
|
|
2136
|
+
"structure",
|
|
2137
|
+
"style",
|
|
2138
|
+
"coverage"
|
|
2139
|
+
]);
|
|
2048
2140
|
var duplicateStringRule = {
|
|
2049
2141
|
id: "logic/duplicate-string",
|
|
2050
2142
|
category: "logic",
|
|
@@ -2075,6 +2167,7 @@ var duplicateStringRule = {
|
|
|
2075
2167
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
2076
2168
|
const value = match[2];
|
|
2077
2169
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2170
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
2078
2171
|
if (value.includes("${")) continue;
|
|
2079
2172
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
2080
2173
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2344,13 +2437,28 @@ var promiseVoidRule = {
|
|
|
2344
2437
|
/^save/,
|
|
2345
2438
|
/^load/,
|
|
2346
2439
|
/^send/,
|
|
2347
|
-
/^delete/,
|
|
2348
2440
|
/^update/,
|
|
2349
2441
|
/^create/,
|
|
2350
2442
|
/^connect/,
|
|
2351
2443
|
/^disconnect/,
|
|
2352
2444
|
/^init/
|
|
2353
2445
|
];
|
|
2446
|
+
const syncMethods = [
|
|
2447
|
+
"delete",
|
|
2448
|
+
// Map.delete(), Set.delete(), Object.delete() are synchronous
|
|
2449
|
+
"has",
|
|
2450
|
+
// Map.has(), Set.has() are synchronous
|
|
2451
|
+
"get",
|
|
2452
|
+
// Map.get() is synchronous
|
|
2453
|
+
"set",
|
|
2454
|
+
// Map.set() is synchronous (though some consider it potentially async)
|
|
2455
|
+
"keys",
|
|
2456
|
+
// Object.keys() is synchronous
|
|
2457
|
+
"values",
|
|
2458
|
+
// Object.values() is synchronous
|
|
2459
|
+
"entries"
|
|
2460
|
+
// Object.entries() is synchronous
|
|
2461
|
+
];
|
|
2354
2462
|
walkAST(ast, (node) => {
|
|
2355
2463
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2356
2464
|
const expr = node.expression;
|
|
@@ -2361,6 +2469,7 @@ var promiseVoidRule = {
|
|
|
2361
2469
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2362
2470
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2363
2471
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2472
|
+
if (syncMethods.includes(fnName)) return;
|
|
2364
2473
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2365
2474
|
const line = node.loc?.start.line ?? 0;
|
|
2366
2475
|
if (line === 0) return;
|
|
@@ -2436,12 +2545,7 @@ var noReassignParamRule = {
|
|
|
2436
2545
|
}
|
|
2437
2546
|
}
|
|
2438
2547
|
if (paramNames.size === 0) return;
|
|
2439
|
-
|
|
2440
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2441
|
-
body = node.value;
|
|
2442
|
-
} else {
|
|
2443
|
-
body = node;
|
|
2444
|
-
}
|
|
2548
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2445
2549
|
if (!body || !("body" in body)) return;
|
|
2446
2550
|
const fnBody = body.body;
|
|
2447
2551
|
if (!fnBody) return;
|
|
@@ -2678,15 +2782,33 @@ var RuleEngine = class {
|
|
|
2678
2782
|
);
|
|
2679
2783
|
}
|
|
2680
2784
|
run(context) {
|
|
2785
|
+
return this.runWithDiagnostics(context).issues;
|
|
2786
|
+
}
|
|
2787
|
+
runWithDiagnostics(context) {
|
|
2681
2788
|
const allIssues = [];
|
|
2789
|
+
const ruleFailures = [];
|
|
2790
|
+
let rulesExecuted = 0;
|
|
2791
|
+
let rulesFailed = 0;
|
|
2682
2792
|
for (const rule of this.rules) {
|
|
2793
|
+
rulesExecuted++;
|
|
2683
2794
|
try {
|
|
2684
2795
|
const issues = rule.check(context);
|
|
2685
2796
|
allIssues.push(...issues);
|
|
2686
|
-
} catch (
|
|
2797
|
+
} catch (err) {
|
|
2798
|
+
rulesFailed++;
|
|
2799
|
+
ruleFailures.push({
|
|
2800
|
+
ruleId: rule.id,
|
|
2801
|
+
file: context.filePath,
|
|
2802
|
+
message: err instanceof Error ? err.message : "Unknown rule execution failure"
|
|
2803
|
+
});
|
|
2687
2804
|
}
|
|
2688
2805
|
}
|
|
2689
|
-
return
|
|
2806
|
+
return {
|
|
2807
|
+
issues: allIssues,
|
|
2808
|
+
rulesExecuted,
|
|
2809
|
+
rulesFailed,
|
|
2810
|
+
ruleFailures
|
|
2811
|
+
};
|
|
2690
2812
|
}
|
|
2691
2813
|
getRules() {
|
|
2692
2814
|
return [...this.rules];
|
|
@@ -3066,6 +3188,8 @@ var PKG_VERSION = (() => {
|
|
|
3066
3188
|
return "0.1.0";
|
|
3067
3189
|
}
|
|
3068
3190
|
})();
|
|
3191
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
3192
|
+
var FINGERPRINT_VERSION = "1";
|
|
3069
3193
|
var ScanEngine = class {
|
|
3070
3194
|
config;
|
|
3071
3195
|
diffParser;
|
|
@@ -3076,81 +3200,191 @@ var ScanEngine = class {
|
|
|
3076
3200
|
this.ruleEngine = new RuleEngine(config);
|
|
3077
3201
|
}
|
|
3078
3202
|
async scan(options) {
|
|
3079
|
-
const
|
|
3203
|
+
const selection = await this.getScanCandidates(options);
|
|
3080
3204
|
const allIssues = [];
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
const lines = hunk.content.split("\n");
|
|
3098
|
-
const result = [];
|
|
3099
|
-
let currentLine = hunk.newStart;
|
|
3100
|
-
for (const line of lines) {
|
|
3101
|
-
if (line.startsWith("+")) {
|
|
3102
|
-
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3103
|
-
currentLine++;
|
|
3104
|
-
} else if (line.startsWith("-")) {
|
|
3105
|
-
} else {
|
|
3106
|
-
currentLine++;
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
return result;
|
|
3110
|
-
});
|
|
3111
|
-
const issues = this.ruleEngine.run({
|
|
3112
|
-
filePath: diffFile.filePath,
|
|
3113
|
-
fileContent,
|
|
3114
|
-
addedLines
|
|
3115
|
-
});
|
|
3116
|
-
allIssues.push(...issues);
|
|
3117
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
3118
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3119
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3120
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3121
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3122
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3123
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
3124
|
-
});
|
|
3125
|
-
allIssues.push(...structureResult.issues);
|
|
3126
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3127
|
-
allIssues.push(...styleResult.issues);
|
|
3128
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3129
|
-
allIssues.push(...coverageResult.issues);
|
|
3205
|
+
const scanErrors = [];
|
|
3206
|
+
const ruleFailures = [];
|
|
3207
|
+
let rulesExecuted = 0;
|
|
3208
|
+
let rulesFailed = 0;
|
|
3209
|
+
let filesScanned = 0;
|
|
3210
|
+
const results = await Promise.all(
|
|
3211
|
+
selection.candidates.map((diffFile) => this.scanFile(diffFile))
|
|
3212
|
+
);
|
|
3213
|
+
for (const result of results) {
|
|
3214
|
+
allIssues.push(...result.issues);
|
|
3215
|
+
ruleFailures.push(...result.ruleFailures);
|
|
3216
|
+
scanErrors.push(...result.scanErrors);
|
|
3217
|
+
rulesExecuted += result.rulesExecuted;
|
|
3218
|
+
rulesFailed += result.rulesFailed;
|
|
3219
|
+
if (result.scanned) {
|
|
3220
|
+
filesScanned++;
|
|
3130
3221
|
}
|
|
3131
3222
|
}
|
|
3132
|
-
const
|
|
3223
|
+
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3224
|
+
const baseline = await this.loadBaseline(options.baseline);
|
|
3225
|
+
const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
|
|
3226
|
+
const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
|
|
3227
|
+
const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
|
|
3228
|
+
const dimensions = this.groupByDimension(issuesWithLifecycle);
|
|
3133
3229
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3134
3230
|
const grade = getGrade(overallScore);
|
|
3135
3231
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
3136
3232
|
return {
|
|
3233
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
3137
3234
|
version: PKG_VERSION,
|
|
3138
3235
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3139
3236
|
commit: commitHash,
|
|
3237
|
+
scanMode: selection.scanMode,
|
|
3140
3238
|
overall: {
|
|
3141
3239
|
score: overallScore,
|
|
3142
3240
|
grade,
|
|
3143
|
-
filesScanned
|
|
3144
|
-
issuesFound:
|
|
3241
|
+
filesScanned,
|
|
3242
|
+
issuesFound: issuesWithLifecycle.length
|
|
3243
|
+
},
|
|
3244
|
+
toolHealth: {
|
|
3245
|
+
rulesExecuted,
|
|
3246
|
+
rulesFailed,
|
|
3247
|
+
filesConsidered: selection.filesConsidered,
|
|
3248
|
+
filesScanned,
|
|
3249
|
+
filesExcluded: selection.filesExcluded,
|
|
3250
|
+
filesSkipped: scanErrors.length,
|
|
3251
|
+
scanErrors,
|
|
3252
|
+
ruleFailures
|
|
3145
3253
|
},
|
|
3146
3254
|
dimensions,
|
|
3147
|
-
issues:
|
|
3255
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3148
3256
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3149
3257
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3150
|
-
})
|
|
3258
|
+
}),
|
|
3259
|
+
lifecycle,
|
|
3260
|
+
fixedIssues
|
|
3151
3261
|
};
|
|
3152
3262
|
}
|
|
3153
|
-
async
|
|
3263
|
+
async scanFile(diffFile) {
|
|
3264
|
+
if (diffFile.status === "deleted") {
|
|
3265
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3266
|
+
}
|
|
3267
|
+
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3268
|
+
return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
|
|
3269
|
+
}
|
|
3270
|
+
const fileContent = await this.readFileContent(diffFile);
|
|
3271
|
+
if (!fileContent) {
|
|
3272
|
+
return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
|
|
3273
|
+
}
|
|
3274
|
+
const addedLines = this.extractAddedLines(diffFile);
|
|
3275
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3276
|
+
filePath: diffFile.filePath,
|
|
3277
|
+
fileContent,
|
|
3278
|
+
addedLines
|
|
3279
|
+
});
|
|
3280
|
+
const issues = [...ruleResult.issues];
|
|
3281
|
+
issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
|
|
3282
|
+
issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
|
|
3283
|
+
issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
|
|
3284
|
+
return {
|
|
3285
|
+
issues,
|
|
3286
|
+
ruleFailures: ruleResult.ruleFailures,
|
|
3287
|
+
rulesExecuted: ruleResult.rulesExecuted,
|
|
3288
|
+
rulesFailed: ruleResult.rulesFailed,
|
|
3289
|
+
scanErrors: [],
|
|
3290
|
+
scanned: true
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
createSkippedResult(diffFile, type, message) {
|
|
3294
|
+
return {
|
|
3295
|
+
issues: [],
|
|
3296
|
+
ruleFailures: [],
|
|
3297
|
+
rulesExecuted: 0,
|
|
3298
|
+
rulesFailed: 0,
|
|
3299
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3300
|
+
scanned: false
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
createErrorResult(diffFile, type, message) {
|
|
3304
|
+
return {
|
|
3305
|
+
issues: [],
|
|
3306
|
+
ruleFailures: [],
|
|
3307
|
+
rulesExecuted: 0,
|
|
3308
|
+
rulesFailed: 0,
|
|
3309
|
+
scanErrors: [{ type, file: diffFile.filePath, message }],
|
|
3310
|
+
scanned: false
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
async readFileContent(diffFile) {
|
|
3314
|
+
const filePath = resolve2(diffFile.filePath);
|
|
3315
|
+
try {
|
|
3316
|
+
return await readFile(filePath, "utf-8");
|
|
3317
|
+
} catch {
|
|
3318
|
+
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3319
|
+
return content ?? null;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
extractAddedLines(diffFile) {
|
|
3323
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3324
|
+
const lines = hunk.content.split("\n");
|
|
3325
|
+
const result = [];
|
|
3326
|
+
let currentLine = hunk.newStart;
|
|
3327
|
+
for (const line of lines) {
|
|
3328
|
+
if (line.startsWith("+")) {
|
|
3329
|
+
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3330
|
+
currentLine++;
|
|
3331
|
+
} else if (line.startsWith("-")) {
|
|
3332
|
+
} else {
|
|
3333
|
+
currentLine++;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return result;
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3340
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3341
|
+
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3342
|
+
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3343
|
+
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3344
|
+
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3345
|
+
maxParamCount: this.config.thresholds["max-params"]
|
|
3346
|
+
}).issues;
|
|
3347
|
+
}
|
|
3348
|
+
async getScanCandidates(options) {
|
|
3349
|
+
const scanMode = this.getScanMode(options);
|
|
3350
|
+
const candidates = await this.getDiffFiles(options);
|
|
3351
|
+
if (scanMode === "files") {
|
|
3352
|
+
return {
|
|
3353
|
+
scanMode,
|
|
3354
|
+
candidates,
|
|
3355
|
+
filesConsidered: options.files?.length ?? candidates.length,
|
|
3356
|
+
filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
const filteredCandidates = [];
|
|
3360
|
+
let filesExcluded = 0;
|
|
3361
|
+
for (const candidate of candidates) {
|
|
3362
|
+
if (this.shouldIncludeFile(candidate.filePath)) {
|
|
3363
|
+
filteredCandidates.push(candidate);
|
|
3364
|
+
} else {
|
|
3365
|
+
filesExcluded++;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return {
|
|
3369
|
+
scanMode,
|
|
3370
|
+
candidates: filteredCandidates,
|
|
3371
|
+
filesConsidered: candidates.length,
|
|
3372
|
+
filesExcluded
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
getScanMode(options) {
|
|
3376
|
+
if (options.staged) {
|
|
3377
|
+
return "staged";
|
|
3378
|
+
}
|
|
3379
|
+
if (options.diff) {
|
|
3380
|
+
return "diff";
|
|
3381
|
+
}
|
|
3382
|
+
if (options.files && options.files.length > 0) {
|
|
3383
|
+
return "files";
|
|
3384
|
+
}
|
|
3385
|
+
return "changed";
|
|
3386
|
+
}
|
|
3387
|
+
getDiffFiles(options) {
|
|
3154
3388
|
if (options.staged) {
|
|
3155
3389
|
return this.diffParser.getStagedFiles();
|
|
3156
3390
|
}
|
|
@@ -3158,14 +3392,10 @@ var ScanEngine = class {
|
|
|
3158
3392
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
3159
3393
|
}
|
|
3160
3394
|
if (options.files && options.files.length > 0) {
|
|
3395
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
3161
3396
|
return Promise.all(
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
try {
|
|
3165
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
3166
|
-
} catch {
|
|
3167
|
-
}
|
|
3168
|
-
return {
|
|
3397
|
+
includedFiles.map(
|
|
3398
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
3169
3399
|
filePath,
|
|
3170
3400
|
status: "modified",
|
|
3171
3401
|
additions: content.split("\n").length,
|
|
@@ -3177,18 +3407,154 @@ var ScanEngine = class {
|
|
|
3177
3407
|
oldLines: 0,
|
|
3178
3408
|
newStart: 1,
|
|
3179
3409
|
newLines: content.split("\n").length,
|
|
3180
|
-
content: content.split("\n").map((
|
|
3410
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
3181
3411
|
}
|
|
3182
3412
|
]
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3413
|
+
}))
|
|
3414
|
+
)
|
|
3185
3415
|
);
|
|
3186
3416
|
}
|
|
3187
3417
|
return this.diffParser.getChangedFiles();
|
|
3188
3418
|
}
|
|
3419
|
+
shouldIncludeFile(filePath) {
|
|
3420
|
+
const normalizedPath = filePath.split(sep).join("/");
|
|
3421
|
+
const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
|
|
3422
|
+
const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3423
|
+
if (!included) {
|
|
3424
|
+
return false;
|
|
3425
|
+
}
|
|
3426
|
+
return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3427
|
+
}
|
|
3428
|
+
matchesPattern(filePath, pattern) {
|
|
3429
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3430
|
+
regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
|
|
3431
|
+
regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
3432
|
+
regexPattern = regexPattern.replace(/\*/g, "[^/]*");
|
|
3433
|
+
regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
|
|
3434
|
+
regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
|
|
3435
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
3436
|
+
}
|
|
3189
3437
|
isTsJsFile(filePath) {
|
|
3190
3438
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
3191
3439
|
}
|
|
3440
|
+
attachFingerprints(issues) {
|
|
3441
|
+
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3442
|
+
return issues.map((issue) => {
|
|
3443
|
+
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3444
|
+
const locationComponent = `${issue.startLine}:${issue.endLine}`;
|
|
3445
|
+
const baseKey = [
|
|
3446
|
+
issue.ruleId,
|
|
3447
|
+
normalizedFile,
|
|
3448
|
+
issue.category,
|
|
3449
|
+
issue.severity,
|
|
3450
|
+
locationComponent
|
|
3451
|
+
].join("|");
|
|
3452
|
+
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3453
|
+
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
3454
|
+
const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
|
|
3455
|
+
return {
|
|
3456
|
+
...issue,
|
|
3457
|
+
file: normalizedFile,
|
|
3458
|
+
fingerprint,
|
|
3459
|
+
fingerprintVersion: FINGERPRINT_VERSION
|
|
3460
|
+
};
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
normalizeRelativePath(filePath) {
|
|
3464
|
+
const absolutePath = resolve2(filePath);
|
|
3465
|
+
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3466
|
+
return relativePath.split(sep).join("/");
|
|
3467
|
+
}
|
|
3468
|
+
async loadBaseline(baselinePath) {
|
|
3469
|
+
if (!baselinePath) {
|
|
3470
|
+
return void 0;
|
|
3471
|
+
}
|
|
3472
|
+
const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
|
|
3473
|
+
const parsed = JSON.parse(baselineContent);
|
|
3474
|
+
const issues = this.parseBaselineIssues(parsed.issues);
|
|
3475
|
+
return {
|
|
3476
|
+
issues,
|
|
3477
|
+
fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
|
|
3478
|
+
commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
|
|
3479
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
parseBaselineIssues(input) {
|
|
3483
|
+
if (!Array.isArray(input)) {
|
|
3484
|
+
return [];
|
|
3485
|
+
}
|
|
3486
|
+
return input.flatMap((item) => {
|
|
3487
|
+
const issue = this.parseBaselineIssue(item);
|
|
3488
|
+
return issue ? [issue] : [];
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
parseBaselineIssue(input) {
|
|
3492
|
+
if (!input || typeof input !== "object") {
|
|
3493
|
+
return void 0;
|
|
3494
|
+
}
|
|
3495
|
+
const issue = input;
|
|
3496
|
+
if (!this.isValidBaselineIssue(issue)) {
|
|
3497
|
+
return void 0;
|
|
3498
|
+
}
|
|
3499
|
+
return {
|
|
3500
|
+
ruleId: issue.ruleId,
|
|
3501
|
+
severity: issue.severity,
|
|
3502
|
+
category: issue.category,
|
|
3503
|
+
file: issue.file,
|
|
3504
|
+
startLine: issue.startLine,
|
|
3505
|
+
endLine: issue.endLine,
|
|
3506
|
+
message: issue.message,
|
|
3507
|
+
fingerprint: issue.fingerprint,
|
|
3508
|
+
fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
isValidBaselineIssue(issue) {
|
|
3512
|
+
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";
|
|
3513
|
+
}
|
|
3514
|
+
attachLifecycle(issues, baseline) {
|
|
3515
|
+
if (!baseline) {
|
|
3516
|
+
return issues;
|
|
3517
|
+
}
|
|
3518
|
+
return issues.map((issue) => ({
|
|
3519
|
+
...issue,
|
|
3520
|
+
lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
|
|
3521
|
+
}));
|
|
3522
|
+
}
|
|
3523
|
+
getFixedIssues(issues, baseline) {
|
|
3524
|
+
if (!baseline) {
|
|
3525
|
+
return [];
|
|
3526
|
+
}
|
|
3527
|
+
const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
|
|
3528
|
+
return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
|
|
3529
|
+
}
|
|
3530
|
+
buildLifecycleSummary(issues, fixedIssues, baseline) {
|
|
3531
|
+
if (!baseline) {
|
|
3532
|
+
return void 0;
|
|
3533
|
+
}
|
|
3534
|
+
let newIssues = 0;
|
|
3535
|
+
let existingIssues = 0;
|
|
3536
|
+
for (const issue of issues) {
|
|
3537
|
+
if (issue.lifecycle === "existing") {
|
|
3538
|
+
existingIssues++;
|
|
3539
|
+
} else {
|
|
3540
|
+
newIssues++;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
return {
|
|
3544
|
+
newIssues,
|
|
3545
|
+
existingIssues,
|
|
3546
|
+
fixedIssues: fixedIssues.length,
|
|
3547
|
+
baselineUsed: true,
|
|
3548
|
+
baselineCommit: baseline.commit,
|
|
3549
|
+
baselineTimestamp: baseline.timestamp
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
isSeverity(value) {
|
|
3553
|
+
return value === "high" || value === "medium" || value === "low" || value === "info";
|
|
3554
|
+
}
|
|
3555
|
+
isRuleCategory(value) {
|
|
3556
|
+
return ["security", "logic", "structure", "style", "coverage"].includes(value);
|
|
3557
|
+
}
|
|
3192
3558
|
groupByDimension(issues) {
|
|
3193
3559
|
const categories = [
|
|
3194
3560
|
"security",
|
|
@@ -3199,7 +3565,7 @@ var ScanEngine = class {
|
|
|
3199
3565
|
];
|
|
3200
3566
|
const grouped = {};
|
|
3201
3567
|
for (const cat of categories) {
|
|
3202
|
-
const catIssues = issues.filter((
|
|
3568
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3203
3569
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3204
3570
|
}
|
|
3205
3571
|
return grouped;
|
|
@@ -3219,7 +3585,13 @@ var en = {
|
|
|
3219
3585
|
issuesFound: "{{count}} issue(s) found",
|
|
3220
3586
|
issuesHeader: "Issues ({{count}}):",
|
|
3221
3587
|
noIssuesFound: "No issues found! \u{1F389}",
|
|
3222
|
-
scanned: "Scanned {{count}} file(s)"
|
|
3588
|
+
scanned: "Scanned {{count}} file(s)",
|
|
3589
|
+
healthHeader: "Tool Health",
|
|
3590
|
+
rulesFailed: "Failed rules: {{count}}",
|
|
3591
|
+
filesSkipped: "Skipped files: {{count}}",
|
|
3592
|
+
filesExcluded: "Excluded files: {{count}}",
|
|
3593
|
+
lifecycleHeader: "Lifecycle",
|
|
3594
|
+
lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
|
|
3223
3595
|
};
|
|
3224
3596
|
var zh = {
|
|
3225
3597
|
reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
|
|
@@ -3231,7 +3603,13 @@ var zh = {
|
|
|
3231
3603
|
issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
|
|
3232
3604
|
issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
|
|
3233
3605
|
noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
|
|
3234
|
-
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
|
|
3606
|
+
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6",
|
|
3607
|
+
healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
|
|
3608
|
+
rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
|
|
3609
|
+
filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3610
|
+
filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3611
|
+
lifecycleHeader: "\u751F\u547D\u5468\u671F",
|
|
3612
|
+
lifecycleSummary: "\u65B0\u589E\uFF1A{{new}} \u5DF2\u5B58\u5728\uFF1A{{existing}} \u5DF2\u4FEE\u590D\uFF1A{{fixed}}"
|
|
3235
3613
|
};
|
|
3236
3614
|
function renderTerminalReport(report) {
|
|
3237
3615
|
const isZh = isZhLocale();
|
|
@@ -3273,19 +3651,37 @@ function renderTerminalReport(report) {
|
|
|
3273
3651
|
};
|
|
3274
3652
|
const dims = ["security", "logic", "structure", "style", "coverage"];
|
|
3275
3653
|
for (const dim of dims) {
|
|
3276
|
-
const
|
|
3277
|
-
const dimEmoji =
|
|
3278
|
-
const color = getScoreColor(
|
|
3279
|
-
const issueCount =
|
|
3654
|
+
const dimension = report.dimensions[dim];
|
|
3655
|
+
const dimEmoji = dimension.score >= 80 ? "\u2705" : dimension.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
|
|
3656
|
+
const color = getScoreColor(dimension.score);
|
|
3657
|
+
const issueCount = dimension.issues.length;
|
|
3280
3658
|
const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
|
|
3281
3659
|
table.push([
|
|
3282
3660
|
`${dimEmoji} ${dimLabels[dim]}`,
|
|
3283
|
-
color(String(
|
|
3661
|
+
color(String(dimension.score)),
|
|
3284
3662
|
detail
|
|
3285
3663
|
]);
|
|
3286
3664
|
}
|
|
3287
3665
|
lines.push(table.toString());
|
|
3288
3666
|
lines.push("");
|
|
3667
|
+
if (report.toolHealth.rulesFailed > 0 || report.toolHealth.filesSkipped > 0 || report.toolHealth.filesExcluded > 0) {
|
|
3668
|
+
lines.push(pc.bold(t2.healthHeader));
|
|
3669
|
+
if (report.toolHealth.rulesFailed > 0) {
|
|
3670
|
+
lines.push(pc.yellow(` ${t2.rulesFailed.replace("{{count}}", String(report.toolHealth.rulesFailed))}`));
|
|
3671
|
+
}
|
|
3672
|
+
if (report.toolHealth.filesSkipped > 0) {
|
|
3673
|
+
lines.push(pc.yellow(` ${t2.filesSkipped.replace("{{count}}", String(report.toolHealth.filesSkipped))}`));
|
|
3674
|
+
}
|
|
3675
|
+
if (report.toolHealth.filesExcluded > 0) {
|
|
3676
|
+
lines.push(pc.dim(` ${t2.filesExcluded.replace("{{count}}", String(report.toolHealth.filesExcluded))}`));
|
|
3677
|
+
}
|
|
3678
|
+
lines.push("");
|
|
3679
|
+
}
|
|
3680
|
+
if (report.lifecycle) {
|
|
3681
|
+
lines.push(pc.bold(t2.lifecycleHeader));
|
|
3682
|
+
lines.push(` ${t2.lifecycleSummary.replace("{{new}}", String(report.lifecycle.newIssues)).replace("{{existing}}", String(report.lifecycle.existingIssues)).replace("{{fixed}}", String(report.lifecycle.fixedIssues))}`);
|
|
3683
|
+
lines.push("");
|
|
3684
|
+
}
|
|
3289
3685
|
if (report.issues.length > 0) {
|
|
3290
3686
|
lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
|
|
3291
3687
|
lines.push("");
|
|
@@ -3349,12 +3745,25 @@ function getScoreColor(score) {
|
|
|
3349
3745
|
|
|
3350
3746
|
// src/cli/output/json.ts
|
|
3351
3747
|
function renderJsonReport(report) {
|
|
3352
|
-
|
|
3748
|
+
const payload = {
|
|
3749
|
+
schemaVersion: report.schemaVersion,
|
|
3750
|
+
version: report.version,
|
|
3751
|
+
timestamp: report.timestamp,
|
|
3752
|
+
commit: report.commit,
|
|
3753
|
+
scanMode: report.scanMode,
|
|
3754
|
+
overall: report.overall,
|
|
3755
|
+
toolHealth: report.toolHealth,
|
|
3756
|
+
dimensions: report.dimensions,
|
|
3757
|
+
issues: report.issues,
|
|
3758
|
+
lifecycle: report.lifecycle,
|
|
3759
|
+
fixedIssues: report.fixedIssues
|
|
3760
|
+
};
|
|
3761
|
+
return JSON.stringify(payload, null, 2);
|
|
3353
3762
|
}
|
|
3354
3763
|
|
|
3355
3764
|
// src/cli/commands/scan.ts
|
|
3356
3765
|
function createScanCommand() {
|
|
3357
|
-
const cmd = new Command("scan").description("
|
|
3766
|
+
const cmd = new Command("scan").description("Run the primary live trust analysis command").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").option("--baseline <path>", "Compare current findings against a prior CodeTrust JSON report").action(async (files, opts) => {
|
|
3358
3767
|
try {
|
|
3359
3768
|
const config = await loadConfig();
|
|
3360
3769
|
const engine = new ScanEngine(config);
|
|
@@ -3363,7 +3772,8 @@ function createScanCommand() {
|
|
|
3363
3772
|
diff: opts.diff,
|
|
3364
3773
|
files: files.length > 0 ? files : void 0,
|
|
3365
3774
|
format: opts.format,
|
|
3366
|
-
minScore: parseInt(opts.minScore, 10)
|
|
3775
|
+
minScore: parseInt(opts.minScore, 10),
|
|
3776
|
+
baseline: opts.baseline
|
|
3367
3777
|
};
|
|
3368
3778
|
const report = await engine.scan(scanOptions);
|
|
3369
3779
|
if (opts.format === "json") {
|
|
@@ -3389,7 +3799,7 @@ function createScanCommand() {
|
|
|
3389
3799
|
// src/cli/commands/report.ts
|
|
3390
3800
|
import { Command as Command2 } from "commander";
|
|
3391
3801
|
function createReportCommand() {
|
|
3392
|
-
const cmd = new Command2("report").description("
|
|
3802
|
+
const cmd = new Command2("report").description("Render a report for a diff-based scan (transitional wrapper around scan)").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref for report presentation", "HEAD~1").action(async (opts) => {
|
|
3393
3803
|
try {
|
|
3394
3804
|
const config = await loadConfig();
|
|
3395
3805
|
const engine = new ScanEngine(config);
|
|
@@ -3413,14 +3823,14 @@ function createReportCommand() {
|
|
|
3413
3823
|
|
|
3414
3824
|
// src/cli/commands/init.ts
|
|
3415
3825
|
import { writeFile } from "fs/promises";
|
|
3416
|
-
import { existsSync as
|
|
3826
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3417
3827
|
import { resolve as resolve3 } from "path";
|
|
3418
3828
|
import { Command as Command3 } from "commander";
|
|
3419
3829
|
import pc2 from "picocolors";
|
|
3420
3830
|
function createInitCommand() {
|
|
3421
3831
|
const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
|
|
3422
3832
|
const configPath = resolve3(".codetrust.yml");
|
|
3423
|
-
if (
|
|
3833
|
+
if (existsSync3(configPath)) {
|
|
3424
3834
|
console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
|
|
3425
3835
|
return;
|
|
3426
3836
|
}
|
|
@@ -3494,7 +3904,7 @@ function formatSeverity2(severity) {
|
|
|
3494
3904
|
|
|
3495
3905
|
// src/cli/commands/hook.ts
|
|
3496
3906
|
import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
|
|
3497
|
-
import { existsSync as
|
|
3907
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3498
3908
|
import { resolve as resolve4, join as join2 } from "path";
|
|
3499
3909
|
import { Command as Command5 } from "commander";
|
|
3500
3910
|
import pc4 from "picocolors";
|
|
@@ -3511,17 +3921,17 @@ function createHookCommand() {
|
|
|
3511
3921
|
const cmd = new Command5("hook").description("Manage git hooks");
|
|
3512
3922
|
cmd.command("install").description("Install pre-commit hook").action(async () => {
|
|
3513
3923
|
const gitDir = resolve4(".git");
|
|
3514
|
-
if (!
|
|
3924
|
+
if (!existsSync4(gitDir)) {
|
|
3515
3925
|
console.error(pc4.red("Error: Not a git repository."));
|
|
3516
3926
|
process.exit(1);
|
|
3517
3927
|
}
|
|
3518
3928
|
const hooksDir = join2(gitDir, "hooks");
|
|
3519
3929
|
const hookPath = join2(hooksDir, "pre-commit");
|
|
3520
3930
|
try {
|
|
3521
|
-
if (!
|
|
3931
|
+
if (!existsSync4(hooksDir)) {
|
|
3522
3932
|
await mkdir(hooksDir, { recursive: true });
|
|
3523
3933
|
}
|
|
3524
|
-
if (
|
|
3934
|
+
if (existsSync4(hookPath)) {
|
|
3525
3935
|
console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
|
|
3526
3936
|
console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
|
|
3527
3937
|
return;
|