@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/index.d.ts
CHANGED
|
@@ -14,9 +14,30 @@ interface Issue {
|
|
|
14
14
|
suggestion?: string;
|
|
15
15
|
codeSnippet?: string;
|
|
16
16
|
}
|
|
17
|
+
type IssueLifecycleStatus = 'new' | 'existing';
|
|
17
18
|
interface ReportIssue extends Issue {
|
|
18
19
|
fingerprint: string;
|
|
19
20
|
fingerprintVersion: string;
|
|
21
|
+
lifecycle?: IssueLifecycleStatus;
|
|
22
|
+
}
|
|
23
|
+
interface FixedIssue {
|
|
24
|
+
ruleId: string;
|
|
25
|
+
severity: Severity;
|
|
26
|
+
category: RuleCategory;
|
|
27
|
+
file: string;
|
|
28
|
+
startLine: number;
|
|
29
|
+
endLine: number;
|
|
30
|
+
message: string;
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
fingerprintVersion?: string;
|
|
33
|
+
}
|
|
34
|
+
interface LifecycleSummary {
|
|
35
|
+
newIssues: number;
|
|
36
|
+
existingIssues: number;
|
|
37
|
+
fixedIssues: number;
|
|
38
|
+
baselineUsed: boolean;
|
|
39
|
+
baselineCommit?: string;
|
|
40
|
+
baselineTimestamp?: string;
|
|
20
41
|
}
|
|
21
42
|
interface DimensionScore {
|
|
22
43
|
score: number;
|
|
@@ -70,6 +91,8 @@ interface TrustReport {
|
|
|
70
91
|
coverage: DimensionScore;
|
|
71
92
|
};
|
|
72
93
|
issues: ReportIssue[];
|
|
94
|
+
lifecycle?: LifecycleSummary;
|
|
95
|
+
fixedIssues?: FixedIssue[];
|
|
73
96
|
}
|
|
74
97
|
interface DiffFile {
|
|
75
98
|
filePath: string;
|
|
@@ -91,6 +114,7 @@ interface ScanOptions {
|
|
|
91
114
|
diff?: string;
|
|
92
115
|
files?: string[];
|
|
93
116
|
minScore?: number;
|
|
117
|
+
baseline?: string;
|
|
94
118
|
format?: 'terminal' | 'json' | 'html';
|
|
95
119
|
}
|
|
96
120
|
|
|
@@ -134,6 +158,11 @@ declare class ScanEngine {
|
|
|
134
158
|
constructor(config: CodeTrustConfig, workDir?: string);
|
|
135
159
|
scan(options: ScanOptions): Promise<TrustReport>;
|
|
136
160
|
private scanFile;
|
|
161
|
+
private createSkippedResult;
|
|
162
|
+
private createErrorResult;
|
|
163
|
+
private readFileContent;
|
|
164
|
+
private extractAddedLines;
|
|
165
|
+
private runStructureAnalysis;
|
|
137
166
|
private getScanCandidates;
|
|
138
167
|
private getScanMode;
|
|
139
168
|
private getDiffFiles;
|
|
@@ -142,6 +171,15 @@ declare class ScanEngine {
|
|
|
142
171
|
private isTsJsFile;
|
|
143
172
|
private attachFingerprints;
|
|
144
173
|
private normalizeRelativePath;
|
|
174
|
+
private loadBaseline;
|
|
175
|
+
private parseBaselineIssues;
|
|
176
|
+
private parseBaselineIssue;
|
|
177
|
+
private isValidBaselineIssue;
|
|
178
|
+
private attachLifecycle;
|
|
179
|
+
private getFixedIssues;
|
|
180
|
+
private buildLifecycleSummary;
|
|
181
|
+
private isSeverity;
|
|
182
|
+
private isRuleCategory;
|
|
145
183
|
private groupByDimension;
|
|
146
184
|
}
|
|
147
185
|
|
|
@@ -196,6 +234,11 @@ declare class DiffParser {
|
|
|
196
234
|
getStagedFiles(): Promise<DiffFile[]>;
|
|
197
235
|
getDiffFromRef(ref: string): Promise<DiffFile[]>;
|
|
198
236
|
getChangedFiles(): Promise<DiffFile[]>;
|
|
237
|
+
/**
|
|
238
|
+
* Merge two sets of diff files, deduplicating by file path.
|
|
239
|
+
* When a file appears in both, merge their hunks and combine stats.
|
|
240
|
+
*/
|
|
241
|
+
private mergeDiffFiles;
|
|
199
242
|
getLastCommitDiff(): Promise<DiffFile[]>;
|
|
200
243
|
getCurrentCommitHash(): Promise<string | undefined>;
|
|
201
244
|
getFileContent(filePath: string): Promise<string | undefined>;
|
package/dist/index.js
CHANGED
|
@@ -7,33 +7,64 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
// src/parsers/diff.ts
|
|
9
9
|
import simpleGit from "simple-git";
|
|
10
|
+
var GIT_DIFF_UNIFIED = "--unified=3";
|
|
11
|
+
var SHORT_HASH_LENGTH = 7;
|
|
10
12
|
var DiffParser = class {
|
|
11
13
|
git;
|
|
12
14
|
constructor(workDir) {
|
|
13
15
|
this.git = simpleGit(workDir);
|
|
14
16
|
}
|
|
15
17
|
async getStagedFiles() {
|
|
16
|
-
const diffDetail = await this.git.diff(["--cached",
|
|
18
|
+
const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
|
|
17
19
|
return this.parseDiffOutput(diffDetail);
|
|
18
20
|
}
|
|
19
21
|
async getDiffFromRef(ref) {
|
|
20
|
-
const diffDetail = await this.git.diff([ref,
|
|
22
|
+
const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
|
|
21
23
|
return this.parseDiffOutput(diffDetail);
|
|
22
24
|
}
|
|
23
25
|
async getChangedFiles() {
|
|
24
|
-
const diffDetail = await this.git.diff([
|
|
25
|
-
const stagedDetail = await this.git.diff(["--cached",
|
|
26
|
-
const
|
|
27
|
-
|
|
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());
|
|
28
59
|
}
|
|
29
60
|
async getLastCommitDiff() {
|
|
30
|
-
const diffDetail = await this.git.diff(["HEAD~1", "HEAD",
|
|
61
|
+
const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
|
|
31
62
|
return this.parseDiffOutput(diffDetail);
|
|
32
63
|
}
|
|
33
64
|
async getCurrentCommitHash() {
|
|
34
65
|
try {
|
|
35
66
|
const hash = await this.git.revparse(["HEAD"]);
|
|
36
|
-
return hash.trim().slice(0,
|
|
67
|
+
return hash.trim().slice(0, SHORT_HASH_LENGTH);
|
|
37
68
|
} catch {
|
|
38
69
|
return void 0;
|
|
39
70
|
}
|
|
@@ -660,6 +691,9 @@ function analyzeFunctionNode(node) {
|
|
|
660
691
|
function calculateCyclomaticComplexity(root) {
|
|
661
692
|
let complexity = 1;
|
|
662
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
|
+
}
|
|
663
697
|
switch (n.type) {
|
|
664
698
|
case AST_NODE_TYPES.IfStatement:
|
|
665
699
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
@@ -688,6 +722,9 @@ function calculateCognitiveComplexity(root) {
|
|
|
688
722
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
689
723
|
depthMap.set(root, 0);
|
|
690
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
|
+
}
|
|
691
728
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
692
729
|
const isNesting = isNestingNode(n);
|
|
693
730
|
const depth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -706,6 +743,9 @@ function calculateMaxNestingDepth(root) {
|
|
|
706
743
|
const depthMap = /* @__PURE__ */ new WeakMap();
|
|
707
744
|
depthMap.set(root, 0);
|
|
708
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
|
+
}
|
|
709
749
|
const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
|
|
710
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;
|
|
711
751
|
const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
|
|
@@ -887,14 +927,19 @@ function stringifyCondition(node) {
|
|
|
887
927
|
case AST_NODE_TYPES.Literal:
|
|
888
928
|
return String(node.value);
|
|
889
929
|
case AST_NODE_TYPES.BinaryExpression:
|
|
930
|
+
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
890
931
|
case AST_NODE_TYPES.LogicalExpression:
|
|
891
932
|
return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
|
|
892
933
|
case AST_NODE_TYPES.UnaryExpression:
|
|
893
934
|
return `${node.operator}${stringifyCondition(node.argument)}`;
|
|
894
935
|
case AST_NODE_TYPES.MemberExpression:
|
|
895
936
|
return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
|
|
896
|
-
case AST_NODE_TYPES.CallExpression:
|
|
897
|
-
|
|
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)}`;
|
|
898
943
|
default:
|
|
899
944
|
return `[${node.type}]`;
|
|
900
945
|
}
|
|
@@ -1055,9 +1100,11 @@ var securityRules = [
|
|
|
1055
1100
|
const issues = [];
|
|
1056
1101
|
const lines = context.fileContent.split("\n");
|
|
1057
1102
|
for (let i = 0; i < lines.length; i++) {
|
|
1058
|
-
const
|
|
1103
|
+
const line = lines[i];
|
|
1104
|
+
const trimmed = line.trim();
|
|
1059
1105
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1060
|
-
|
|
1106
|
+
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
|
|
1107
|
+
if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
|
|
1061
1108
|
issues.push({
|
|
1062
1109
|
ruleId: "security/dangerous-html",
|
|
1063
1110
|
severity: "medium",
|
|
@@ -1585,6 +1632,9 @@ var missingAwaitRule = {
|
|
|
1585
1632
|
const body = getFunctionBody(node);
|
|
1586
1633
|
if (!body) return;
|
|
1587
1634
|
walkAST(body, (inner, parent) => {
|
|
1635
|
+
if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1588
1638
|
if (inner !== body && isAsyncFunction(inner)) return false;
|
|
1589
1639
|
if (inner.type !== AST_NODE_TYPES.CallExpression) return;
|
|
1590
1640
|
if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
|
|
@@ -1594,6 +1644,9 @@ var missingAwaitRule = {
|
|
|
1594
1644
|
if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
|
|
1595
1645
|
if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
|
|
1596
1646
|
if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
|
|
1647
|
+
if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1597
1650
|
const callName = getCallName(inner);
|
|
1598
1651
|
if (!callName) return;
|
|
1599
1652
|
if (!asyncFuncNames.has(callName)) return;
|
|
@@ -1805,9 +1858,28 @@ var typeCoercionRule = {
|
|
|
1805
1858
|
var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
|
|
1806
1859
|
-1,
|
|
1807
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,
|
|
1808
1870
|
1,
|
|
1809
1871
|
2,
|
|
1872
|
+
3,
|
|
1873
|
+
4,
|
|
1874
|
+
5,
|
|
1810
1875
|
10,
|
|
1876
|
+
15,
|
|
1877
|
+
20,
|
|
1878
|
+
30,
|
|
1879
|
+
40,
|
|
1880
|
+
50,
|
|
1881
|
+
70,
|
|
1882
|
+
90,
|
|
1811
1883
|
100
|
|
1812
1884
|
]);
|
|
1813
1885
|
var magicNumberRule = {
|
|
@@ -1836,6 +1908,7 @@ var magicNumberRule = {
|
|
|
1836
1908
|
if (/^\s*(export\s+)?enum\s/.test(line)) continue;
|
|
1837
1909
|
if (trimmed.startsWith("import ")) continue;
|
|
1838
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;
|
|
1839
1912
|
const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
|
|
1840
1913
|
const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
|
|
1841
1914
|
let match;
|
|
@@ -1932,6 +2005,17 @@ var nestedTernaryRule = {
|
|
|
1932
2005
|
// src/rules/builtin/duplicate-string.ts
|
|
1933
2006
|
var MIN_STRING_LENGTH = 6;
|
|
1934
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
|
+
]);
|
|
1935
2019
|
var duplicateStringRule = {
|
|
1936
2020
|
id: "logic/duplicate-string",
|
|
1937
2021
|
category: "logic",
|
|
@@ -1962,6 +2046,7 @@ var duplicateStringRule = {
|
|
|
1962
2046
|
while ((match = stringRegex.exec(cleaned)) !== null) {
|
|
1963
2047
|
const value = match[2];
|
|
1964
2048
|
if (value.length < MIN_STRING_LENGTH) continue;
|
|
2049
|
+
if (IGNORED_LITERALS.has(value)) continue;
|
|
1965
2050
|
if (value.includes("${")) continue;
|
|
1966
2051
|
if (value.startsWith("http") || value.startsWith("/")) continue;
|
|
1967
2052
|
if (value.startsWith("test") || value.startsWith("mock")) continue;
|
|
@@ -2231,13 +2316,28 @@ var promiseVoidRule = {
|
|
|
2231
2316
|
/^save/,
|
|
2232
2317
|
/^load/,
|
|
2233
2318
|
/^send/,
|
|
2234
|
-
/^delete/,
|
|
2235
2319
|
/^update/,
|
|
2236
2320
|
/^create/,
|
|
2237
2321
|
/^connect/,
|
|
2238
2322
|
/^disconnect/,
|
|
2239
2323
|
/^init/
|
|
2240
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
|
+
];
|
|
2241
2341
|
walkAST(ast, (node) => {
|
|
2242
2342
|
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
|
|
2243
2343
|
const expr = node.expression;
|
|
@@ -2248,6 +2348,7 @@ var promiseVoidRule = {
|
|
|
2248
2348
|
const isKnownAsync = asyncFnNames.has(fnName);
|
|
2249
2349
|
const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
|
|
2250
2350
|
const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
|
|
2351
|
+
if (syncMethods.includes(fnName)) return;
|
|
2251
2352
|
if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
|
|
2252
2353
|
const line = node.loc?.start.line ?? 0;
|
|
2253
2354
|
if (line === 0) return;
|
|
@@ -2999,7 +3100,11 @@ var ScanEngine = class {
|
|
|
2999
3100
|
}
|
|
3000
3101
|
}
|
|
3001
3102
|
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3002
|
-
const
|
|
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);
|
|
3003
3108
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3004
3109
|
const grade = getGrade(overallScore);
|
|
3005
3110
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
@@ -3013,7 +3118,7 @@ var ScanEngine = class {
|
|
|
3013
3118
|
score: overallScore,
|
|
3014
3119
|
grade,
|
|
3015
3120
|
filesScanned,
|
|
3016
|
-
issuesFound:
|
|
3121
|
+
issuesFound: issuesWithLifecycle.length
|
|
3017
3122
|
},
|
|
3018
3123
|
toolHealth: {
|
|
3019
3124
|
rulesExecuted,
|
|
@@ -3026,70 +3131,75 @@ var ScanEngine = class {
|
|
|
3026
3131
|
ruleFailures
|
|
3027
3132
|
},
|
|
3028
3133
|
dimensions,
|
|
3029
|
-
issues:
|
|
3134
|
+
issues: issuesWithLifecycle.sort((a, b) => {
|
|
3030
3135
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3031
3136
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3032
|
-
})
|
|
3137
|
+
}),
|
|
3138
|
+
lifecycle,
|
|
3139
|
+
fixedIssues
|
|
3033
3140
|
};
|
|
3034
3141
|
}
|
|
3035
3142
|
async scanFile(diffFile) {
|
|
3036
3143
|
if (diffFile.status === "deleted") {
|
|
3037
|
-
return {
|
|
3038
|
-
issues: [],
|
|
3039
|
-
ruleFailures: [],
|
|
3040
|
-
rulesExecuted: 0,
|
|
3041
|
-
rulesFailed: 0,
|
|
3042
|
-
scanErrors: [
|
|
3043
|
-
{
|
|
3044
|
-
type: "deleted-file",
|
|
3045
|
-
file: diffFile.filePath,
|
|
3046
|
-
message: `Skipped deleted file: ${diffFile.filePath}`
|
|
3047
|
-
}
|
|
3048
|
-
],
|
|
3049
|
-
scanned: false
|
|
3050
|
-
};
|
|
3144
|
+
return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
|
|
3051
3145
|
}
|
|
3052
3146
|
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3053
|
-
return {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
scanErrors: [
|
|
3059
|
-
{
|
|
3060
|
-
type: "unsupported-file-type",
|
|
3061
|
-
file: diffFile.filePath,
|
|
3062
|
-
message: `Skipped unsupported file type: ${diffFile.filePath}`
|
|
3063
|
-
}
|
|
3064
|
-
],
|
|
3065
|
-
scanned: false
|
|
3066
|
-
};
|
|
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}`);
|
|
3067
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) {
|
|
3068
3193
|
const filePath = resolve2(diffFile.filePath);
|
|
3069
|
-
let fileContent;
|
|
3070
3194
|
try {
|
|
3071
|
-
|
|
3195
|
+
return await readFile(filePath, "utf-8");
|
|
3072
3196
|
} catch {
|
|
3073
3197
|
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3074
|
-
|
|
3075
|
-
return {
|
|
3076
|
-
issues: [],
|
|
3077
|
-
ruleFailures: [],
|
|
3078
|
-
rulesExecuted: 0,
|
|
3079
|
-
rulesFailed: 0,
|
|
3080
|
-
scanErrors: [
|
|
3081
|
-
{
|
|
3082
|
-
type: "missing-file-content",
|
|
3083
|
-
file: diffFile.filePath,
|
|
3084
|
-
message: `Unable to read file content for ${diffFile.filePath}`
|
|
3085
|
-
}
|
|
3086
|
-
],
|
|
3087
|
-
scanned: false
|
|
3088
|
-
};
|
|
3089
|
-
}
|
|
3090
|
-
fileContent = content;
|
|
3198
|
+
return content ?? null;
|
|
3091
3199
|
}
|
|
3092
|
-
|
|
3200
|
+
}
|
|
3201
|
+
extractAddedLines(diffFile) {
|
|
3202
|
+
return diffFile.hunks.flatMap((hunk) => {
|
|
3093
3203
|
const lines = hunk.content.split("\n");
|
|
3094
3204
|
const result = [];
|
|
3095
3205
|
let currentLine = hunk.newStart;
|
|
@@ -3104,32 +3214,15 @@ var ScanEngine = class {
|
|
|
3104
3214
|
}
|
|
3105
3215
|
return result;
|
|
3106
3216
|
});
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
addedLines
|
|
3111
|
-
});
|
|
3112
|
-
const issues = [...ruleResult.issues];
|
|
3113
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3217
|
+
}
|
|
3218
|
+
runStructureAnalysis(fileContent, filePath) {
|
|
3219
|
+
return analyzeStructure(fileContent, filePath, {
|
|
3114
3220
|
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3115
3221
|
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3116
3222
|
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3117
3223
|
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3118
3224
|
maxParamCount: this.config.thresholds["max-params"]
|
|
3119
|
-
});
|
|
3120
|
-
issues.push(...structureResult.issues);
|
|
3121
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3122
|
-
issues.push(...styleResult.issues);
|
|
3123
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3124
|
-
issues.push(...coverageResult.issues);
|
|
3125
|
-
return {
|
|
3126
|
-
issues,
|
|
3127
|
-
ruleFailures: ruleResult.ruleFailures,
|
|
3128
|
-
rulesExecuted: ruleResult.rulesExecuted,
|
|
3129
|
-
rulesFailed: ruleResult.rulesFailed,
|
|
3130
|
-
scanErrors: [],
|
|
3131
|
-
scanned: true
|
|
3132
|
-
};
|
|
3225
|
+
}).issues;
|
|
3133
3226
|
}
|
|
3134
3227
|
async getScanCandidates(options) {
|
|
3135
3228
|
const scanMode = this.getScanMode(options);
|
|
@@ -3170,7 +3263,7 @@ var ScanEngine = class {
|
|
|
3170
3263
|
}
|
|
3171
3264
|
return "changed";
|
|
3172
3265
|
}
|
|
3173
|
-
|
|
3266
|
+
getDiffFiles(options) {
|
|
3174
3267
|
if (options.staged) {
|
|
3175
3268
|
return this.diffParser.getStagedFiles();
|
|
3176
3269
|
}
|
|
@@ -3251,6 +3344,96 @@ var ScanEngine = class {
|
|
|
3251
3344
|
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3252
3345
|
return relativePath.split(sep).join("/");
|
|
3253
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
|
+
}
|
|
3254
3437
|
groupByDimension(issues) {
|
|
3255
3438
|
const categories = [
|
|
3256
3439
|
"security",
|