@gulu9527/code-trust 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +287 -92
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.js +270 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -128,33 +128,64 @@ import { fileURLToPath } from "url";
|
|
|
128
128
|
|
|
129
129
|
// src/parsers/diff.ts
|
|
130
130
|
import simpleGit from "simple-git";
|
|
131
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
132
|
+
var SHORT_HASH_LENGTH = 7;
|
|
131
133
|
var DiffParser = class {
|
|
132
134
|
git;
|
|
133
135
|
constructor(workDir) {
|
|
134
136
|
this.git = simpleGit(workDir);
|
|
135
137
|
}
|
|
136
138
|
async getStagedFiles() {
|
|
137
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
139
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
138
140
|
return this.parseDiffOutput(diffDetail);
|
|
139
141
|
}
|
|
140
142
|
async getDiffFromRef(ref) {
|
|
141
|
-
const diffDetail = await this.git.diff([ref,
|
|
143
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
142
144
|
return this.parseDiffOutput(diffDetail);
|
|
143
145
|
}
|
|
144
146
|
async getChangedFiles() {
|
|
145
|
-
const diffDetail = await this.git.diff([
|
|
146
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
147
|
-
const
|
|
148
|
-
|
|
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());
|
|
149
180
|
}
|
|
150
181
|
async getLastCommitDiff() {
|
|
151
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
182
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
152
183
|
return this.parseDiffOutput(diffDetail);
|
|
153
184
|
}
|
|
154
185
|
async getCurrentCommitHash() {
|
|
155
186
|
try {
|
|
156
187
|
const hash = await this.git.revparse(["HEAD"]);
|
|
157
|
-
return hash.trim().slice(0,
|
|
188
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
158
189
|
} catch {
|
|
159
190
|
return void 0;
|
|
160
191
|
}
|
|
@@ -781,6 +812,9 @@ function analyzeFunctionNode(node) {
|
|
|
781
812
|
function calculateCyclomaticComplexity(root) {
|
|
782
813
|
let complexity = 1;
|
|
783
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
|
+
}
|
|
784
818
|
switch (n.type) {
|
|
785
819
|
case AST_NODE_TYPES.IfStatement:
|
|
786
820
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -809,6 +843,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
809
843
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
810
844
|
depthMap.set(root, 0);
|
|
811
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
|
+
}
|
|
812
849
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
813
850
|
const isNesting = isNestingNode(n);
|
|
814
851
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -827,6 +864,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
827
864
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
828
865
|
depthMap.set(root, 0);
|
|
829
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
|
+
}
|
|
830
870
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
831
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;
|
|
832
872
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -1008,14 +1048,19 @@ function stringifyCondition(node) {
|
|
|
1008
1048
|
case AST_NODE_TYPES.Literal:
|
|
1009
1049
|
return String(node.value);
|
|
1010
1050
|
case AST_NODE_TYPES.BinaryExpression:
|
|
1051
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1011
1052
|
case AST_NODE_TYPES.LogicalExpression:
|
|
1012
1053
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
1013
1054
|
case AST_NODE_TYPES.UnaryExpression:
|
|
1014
1055
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
1015
1056
|
case AST_NODE_TYPES.MemberExpression:
|
|
1016
1057
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
1017
|
-
case AST_NODE_TYPES.CallExpression:
|
|
1018
|
-
|
|
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)}`;
|
|
1019
1064
|
default:
|
|
1020
1065
|
return `[${node.type}]`;
|
|
1021
1066
|
}
|
|
@@ -1176,9 +1221,11 @@ var securityRules = [
|
|
|
1176
1221
|
const issues = [];
|
|
1177
1222
|
const lines = context.fileContent.split("\n");
|
|
1178
1223
|
for (let i = 0; i < lines.length; i++) {
|
|
1179
|
-
const
|
|
1224
|
+
const line = lines[i];
|
|
1225
|
+
const trimmed = line.trim();
|
|
1180
1226
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1181
|
-
|
|
1227
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1228
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1182
1229
|
issues.push({
|
|
1183
1230
|
ruleId: "security/dangerous-html",
|
|
1184
1231
|
severity: "medium",
|
|
@@ -1706,6 +1753,9 @@ var missingAwaitRule = {
|
|
|
1706
1753
|
const body = getFunctionBody(node);
|
|
1707
1754
|
if (!body) return;
|
|
1708
1755
|
walkAST(body, (inner, parent) => {
|
|
1756
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1709
1759
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1710
1760
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1711
1761
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1715,6 +1765,9 @@ var missingAwaitRule = {
|
|
|
1715
1765
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1716
1766
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1717
1767
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1768
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1718
1771
|
const callName = getCallName(inner);
|
|
1719
1772
|
if (!callName) return;
|
|
1720
1773
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1926,9 +1979,28 @@ var typeCoercionRule = {
|
|
|
1926
1979
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1927
1980
|
-1,
|
|
1928
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,
|
|
1929
1991
|
1,
|
|
1930
1992
|
2,
|
|
1993
|
+
3,
|
|
1994
|
+
4,
|
|
1995
|
+
5,
|
|
1931
1996
|
10,
|
|
1997
|
+
15,
|
|
1998
|
+
20,
|
|
1999
|
+
30,
|
|
2000
|
+
40,
|
|
2001
|
+
50,
|
|
2002
|
+
70,
|
|
2003
|
+
90,
|
|
1932
2004
|
100
|
|
1933
2005
|
]);
|
|
1934
2006
|
var magicNumberRule = {
|
|
@@ -1957,6 +2029,7 @@ var magicNumberRule = {
|
|
|
1957
2029
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1958
2030
|
if (trimmed.startsWith("import ")) continue;
|
|
1959
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;
|
|
1960
2033
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1961
2034
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1962
2035
|
let match;
|
|
@@ -2053,6 +2126,17 @@ var nestedTernaryRule = {
|
|
|
2053
2126
|
// src/rules/builtin/duplicate-string.ts
|
|
2054
2127
|
var MIN_STRING_LENGTH = 6;
|
|
2055
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
|
+
]);
|
|
2056
2140
|
var duplicateStringRule = {
|
|
2057
2141
|
id: "logic/duplicate-string",
|
|
2058
2142
|
category: "logic",
|
|
@@ -2083,6 +2167,7 @@ var duplicateStringRule = {
|
|
|
2083
2167
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
2084
2168
|
const value = match[2];
|
|
2085
2169
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2170
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
2086
2171
|
if (value.includes("${")) continue;
|
|
2087
2172
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
2088
2173
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2352,13 +2437,28 @@ var promiseVoidRule = {
|
|
|
2352
2437
|
/^save/,
|
|
2353
2438
|
/^load/,
|
|
2354
2439
|
/^send/,
|
|
2355
|
-
/^delete/,
|
|
2356
2440
|
/^update/,
|
|
2357
2441
|
/^create/,
|
|
2358
2442
|
/^connect/,
|
|
2359
2443
|
/^disconnect/,
|
|
2360
2444
|
/^init/
|
|
2361
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
|
+
];
|
|
2362
2462
|
walkAST(ast, (node) => {
|
|
2363
2463
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2364
2464
|
const expr = node.expression;
|
|
@@ -2369,6 +2469,7 @@ var promiseVoidRule = {
|
|
|
2369
2469
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2370
2470
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2371
2471
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2472
|
+
if (syncMethods.includes(fnName)) return;
|
|
2372
2473
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2373
2474
|
const line = node.loc?.start.line ?? 0;
|
|
2374
2475
|
if (line === 0) return;
|
|
@@ -3120,7 +3221,11 @@ var ScanEngine = class {
|
|
|
3120
3221
|
}
|
|
3121
3222
|
}
|
|
3122
3223
|
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3123
|
-
const
|
|
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);
|
|
3124
3229
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3125
3230
|
const grade = getGrade(overallScore);
|
|
3126
3231
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
@@ -3134,7 +3239,7 @@ var ScanEngine = class {
|
|
|
3134
3239
|
score: overallScore,
|
|
3135
3240
|
grade,
|
|
3136
3241
|
filesScanned,
|
|
3137
|
-
issuesFound:
|
|
3242
|
+
issuesFound: issuesWithLifecycle.length
|
|
3138
3243
|
},
|
|
3139
3244
|
toolHealth: {
|
|
3140
3245
|
rulesExecuted,
|
|
@@ -3147,70 +3252,75 @@ var ScanEngine = class {
|
|
|
3147
3252
|
ruleFailures
|
|
3148
3253
|
},
|
|
3149
3254
|
dimensions,
|
|
3150
|
-
issues:
|
|
3255
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3151
3256
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3152
3257
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3153
|
-
})
|
|
3258
|
+
}),
|
|
3259
|
+
lifecycle,
|
|
3260
|
+
fixedIssues
|
|
3154
3261
|
};
|
|
3155
3262
|
}
|
|
3156
3263
|
async scanFile(diffFile) {
|
|
3157
3264
|
if (diffFile.status === "deleted") {
|
|
3158
|
-
return {
|
|
3159
|
-
issues: [],
|
|
3160
|
-
ruleFailures: [],
|
|
3161
|
-
rulesExecuted: 0,
|
|
3162
|
-
rulesFailed: 0,
|
|
3163
|
-
scanErrors: [
|
|
3164
|
-
{
|
|
3165
|
-
type: "deleted-file",
|
|
3166
|
-
file: diffFile.filePath,
|
|
3167
|
-
message: `Skipped deleted file: ${diffFile.filePath}`
|
|
3168
|
-
}
|
|
3169
|
-
],
|
|
3170
|
-
scanned: false
|
|
3171
|
-
};
|
|
3265
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3172
3266
|
}
|
|
3173
3267
|
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3174
|
-
return {
|
|
3175
|
-
issues: [],
|
|
3176
|
-
ruleFailures: [],
|
|
3177
|
-
rulesExecuted: 0,
|
|
3178
|
-
rulesFailed: 0,
|
|
3179
|
-
scanErrors: [
|
|
3180
|
-
{
|
|
3181
|
-
type: "unsupported-file-type",
|
|
3182
|
-
file: diffFile.filePath,
|
|
3183
|
-
message: `Skipped unsupported file type: ${diffFile.filePath}`
|
|
3184
|
-
}
|
|
3185
|
-
],
|
|
3186
|
-
scanned: false
|
|
3187
|
-
};
|
|
3268
|
+
return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
|
|
3188
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) {
|
|
3189
3314
|
const filePath = resolve2(diffFile.filePath);
|
|
3190
|
-
let fileContent;
|
|
3191
3315
|
try {
|
|
3192
|
-
|
|
3316
|
+
return await readFile(filePath, "utf-8");
|
|
3193
3317
|
} catch {
|
|
3194
3318
|
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3195
|
-
|
|
3196
|
-
return {
|
|
3197
|
-
issues: [],
|
|
3198
|
-
ruleFailures: [],
|
|
3199
|
-
rulesExecuted: 0,
|
|
3200
|
-
rulesFailed: 0,
|
|
3201
|
-
scanErrors: [
|
|
3202
|
-
{
|
|
3203
|
-
type: "missing-file-content",
|
|
3204
|
-
file: diffFile.filePath,
|
|
3205
|
-
message: `Unable to read file content for ${diffFile.filePath}`
|
|
3206
|
-
}
|
|
3207
|
-
],
|
|
3208
|
-
scanned: false
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3211
|
-
fileContent = content;
|
|
3319
|
+
return content ?? null;
|
|
3212
3320
|
}
|
|
3213
|
-
|
|
3321
|
+
}
|
|
3322
|
+
extractAddedLines(diffFile) {
|
|
3323
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3214
3324
|
const lines = hunk.content.split("\n");
|
|
3215
3325
|
const result = [];
|
|
3216
3326
|
let currentLine = hunk.newStart;
|
|
@@ -3225,32 +3335,15 @@ var ScanEngine = class {
|
|
|
3225
3335
|
}
|
|
3226
3336
|
return result;
|
|
3227
3337
|
});
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
addedLines
|
|
3232
|
-
});
|
|
3233
|
-
const issues = [...ruleResult.issues];
|
|
3234
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3338
|
+
}
|
|
3339
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3340
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3235
3341
|
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3236
3342
|
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3237
3343
|
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3238
3344
|
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3239
3345
|
maxParamCount: this.config.thresholds["max-params"]
|
|
3240
|
-
});
|
|
3241
|
-
issues.push(...structureResult.issues);
|
|
3242
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3243
|
-
issues.push(...styleResult.issues);
|
|
3244
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3245
|
-
issues.push(...coverageResult.issues);
|
|
3246
|
-
return {
|
|
3247
|
-
issues,
|
|
3248
|
-
ruleFailures: ruleResult.ruleFailures,
|
|
3249
|
-
rulesExecuted: ruleResult.rulesExecuted,
|
|
3250
|
-
rulesFailed: ruleResult.rulesFailed,
|
|
3251
|
-
scanErrors: [],
|
|
3252
|
-
scanned: true
|
|
3253
|
-
};
|
|
3346
|
+
}).issues;
|
|
3254
3347
|
}
|
|
3255
3348
|
async getScanCandidates(options) {
|
|
3256
3349
|
const scanMode = this.getScanMode(options);
|
|
@@ -3291,7 +3384,7 @@ var ScanEngine = class {
|
|
|
3291
3384
|
}
|
|
3292
3385
|
return "changed";
|
|
3293
3386
|
}
|
|
3294
|
-
|
|
3387
|
+
getDiffFiles(options) {
|
|
3295
3388
|
if (options.staged) {
|
|
3296
3389
|
return this.diffParser.getStagedFiles();
|
|
3297
3390
|
}
|
|
@@ -3372,6 +3465,96 @@ var ScanEngine = class {
|
|
|
3372
3465
|
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3373
3466
|
return relativePath.split(sep).join("/");
|
|
3374
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
|
+
}
|
|
3375
3558
|
groupByDimension(issues) {
|
|
3376
3559
|
const categories = [
|
|
3377
3560
|
"security",
|
|
@@ -3406,7 +3589,9 @@ var en = {
|
|
|
3406
3589
|
healthHeader: "Tool Health",
|
|
3407
3590
|
rulesFailed: "Failed rules: {{count}}",
|
|
3408
3591
|
filesSkipped: "Skipped files: {{count}}",
|
|
3409
|
-
filesExcluded: "Excluded files: {{count}}"
|
|
3592
|
+
filesExcluded: "Excluded files: {{count}}",
|
|
3593
|
+
lifecycleHeader: "Lifecycle",
|
|
3594
|
+
lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
|
|
3410
3595
|
};
|
|
3411
3596
|
var zh = {
|
|
3412
3597
|
reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
|
|
@@ -3422,7 +3607,9 @@ var zh = {
|
|
|
3422
3607
|
healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
|
|
3423
3608
|
rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
|
|
3424
3609
|
filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3425
|
-
filesExcluded: "\u6392\u9664\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}}"
|
|
3426
3613
|
};
|
|
3427
3614
|
function renderTerminalReport(report) {
|
|
3428
3615
|
const isZh = isZhLocale();
|
|
@@ -3490,6 +3677,11 @@ function renderTerminalReport(report) {
|
|
|
3490
3677
|
}
|
|
3491
3678
|
lines.push("");
|
|
3492
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
|
+
}
|
|
3493
3685
|
if (report.issues.length > 0) {
|
|
3494
3686
|
lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
|
|
3495
3687
|
lines.push("");
|
|
@@ -3562,14 +3754,16 @@ function renderJsonReport(report) {
|
|
|
3562
3754
|
overall: report.overall,
|
|
3563
3755
|
toolHealth: report.toolHealth,
|
|
3564
3756
|
dimensions: report.dimensions,
|
|
3565
|
-
issues: report.issues
|
|
3757
|
+
issues: report.issues,
|
|
3758
|
+
lifecycle: report.lifecycle,
|
|
3759
|
+
fixedIssues: report.fixedIssues
|
|
3566
3760
|
};
|
|
3567
3761
|
return JSON.stringify(payload, null, 2);
|
|
3568
3762
|
}
|
|
3569
3763
|
|
|
3570
3764
|
// src/cli/commands/scan.ts
|
|
3571
3765
|
function createScanCommand() {
|
|
3572
|
-
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").action(async (files, opts) => {
|
|
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) => {
|
|
3573
3767
|
try {
|
|
3574
3768
|
const config = await loadConfig();
|
|
3575
3769
|
const engine = new ScanEngine(config);
|
|
@@ -3578,7 +3772,8 @@ function createScanCommand() {
|
|
|
3578
3772
|
diff: opts.diff,
|
|
3579
3773
|
files: files.length > 0 ? files : void 0,
|
|
3580
3774
|
format: opts.format,
|
|
3581
|
-
minScore: parseInt(opts.minScore, 10)
|
|
3775
|
+
minScore: parseInt(opts.minScore, 10),
|
|
3776
|
+
baseline: opts.baseline
|
|
3582
3777
|
};
|
|
3583
3778
|
const report = await engine.scan(scanOptions);
|
|
3584
3779
|
if (opts.format === "json") {
|