@gulu9527/code-trust 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +256 -0
- package/README.md +42 -4
- package/dist/cli/index.js +337 -122
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +46 -2
- package/dist/index.js +291 -108
- 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/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type Severity = 'high' | 'medium' | 'low' | 'info';
|
|
2
2
|
type RuleCategory = 'security' | 'logic' | 'structure' | 'style' | 'coverage';
|
|
3
3
|
type TrustGrade = 'HIGH_TRUST' | 'REVIEW' | 'LOW_TRUST' | 'UNTRUSTED';
|
|
4
|
+
type ScanMode = 'staged' | 'diff' | 'files' | 'changed';
|
|
5
|
+
type ScanErrorType = 'rule-failure' | 'deleted-file' | 'unreadable-file' | 'missing-file-content' | 'unsupported-file-type';
|
|
4
6
|
interface Issue {
|
|
5
7
|
ruleId: string;
|
|
6
8
|
severity: Severity;
|
|
@@ -12,20 +14,54 @@ interface Issue {
|
|
|
12
14
|
suggestion?: string;
|
|
13
15
|
codeSnippet?: string;
|
|
14
16
|
}
|
|
17
|
+
interface ReportIssue extends Issue {
|
|
18
|
+
fingerprint: string;
|
|
19
|
+
fingerprintVersion: string;
|
|
20
|
+
}
|
|
15
21
|
interface DimensionScore {
|
|
16
22
|
score: number;
|
|
23
|
+
issues: ReportIssue[];
|
|
24
|
+
}
|
|
25
|
+
interface RuleFailure {
|
|
26
|
+
ruleId: string;
|
|
27
|
+
file: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
interface RuleRunResult {
|
|
17
31
|
issues: Issue[];
|
|
32
|
+
rulesExecuted: number;
|
|
33
|
+
rulesFailed: number;
|
|
34
|
+
ruleFailures: RuleFailure[];
|
|
35
|
+
}
|
|
36
|
+
interface ScanError {
|
|
37
|
+
type: ScanErrorType;
|
|
38
|
+
message: string;
|
|
39
|
+
file?: string;
|
|
40
|
+
ruleId?: string;
|
|
41
|
+
}
|
|
42
|
+
interface ToolHealth {
|
|
43
|
+
rulesExecuted: number;
|
|
44
|
+
rulesFailed: number;
|
|
45
|
+
filesConsidered: number;
|
|
46
|
+
filesScanned: number;
|
|
47
|
+
filesExcluded: number;
|
|
48
|
+
filesSkipped: number;
|
|
49
|
+
scanErrors: ScanError[];
|
|
50
|
+
ruleFailures: RuleFailure[];
|
|
18
51
|
}
|
|
19
52
|
interface TrustReport {
|
|
53
|
+
schemaVersion: string;
|
|
20
54
|
version: string;
|
|
21
55
|
timestamp: string;
|
|
22
56
|
commit?: string;
|
|
57
|
+
scanMode: ScanMode;
|
|
23
58
|
overall: {
|
|
24
59
|
score: number;
|
|
25
60
|
grade: TrustGrade;
|
|
26
61
|
filesScanned: number;
|
|
27
62
|
issuesFound: number;
|
|
28
63
|
};
|
|
64
|
+
toolHealth: ToolHealth;
|
|
29
65
|
dimensions: {
|
|
30
66
|
security: DimensionScore;
|
|
31
67
|
logic: DimensionScore;
|
|
@@ -33,7 +69,7 @@ interface TrustReport {
|
|
|
33
69
|
style: DimensionScore;
|
|
34
70
|
coverage: DimensionScore;
|
|
35
71
|
};
|
|
36
|
-
issues:
|
|
72
|
+
issues: ReportIssue[];
|
|
37
73
|
}
|
|
38
74
|
interface DiffFile {
|
|
39
75
|
filePath: string;
|
|
@@ -97,8 +133,15 @@ declare class ScanEngine {
|
|
|
97
133
|
private ruleEngine;
|
|
98
134
|
constructor(config: CodeTrustConfig, workDir?: string);
|
|
99
135
|
scan(options: ScanOptions): Promise<TrustReport>;
|
|
136
|
+
private scanFile;
|
|
137
|
+
private getScanCandidates;
|
|
138
|
+
private getScanMode;
|
|
100
139
|
private getDiffFiles;
|
|
140
|
+
private shouldIncludeFile;
|
|
141
|
+
private matchesPattern;
|
|
101
142
|
private isTsJsFile;
|
|
143
|
+
private attachFingerprints;
|
|
144
|
+
private normalizeRelativePath;
|
|
102
145
|
private groupByDimension;
|
|
103
146
|
}
|
|
104
147
|
|
|
@@ -137,6 +180,7 @@ declare class RuleEngine {
|
|
|
137
180
|
private rules;
|
|
138
181
|
constructor(config: CodeTrustConfig);
|
|
139
182
|
run(context: RuleContext): Issue[];
|
|
183
|
+
runWithDiagnostics(context: RuleContext): RuleRunResult;
|
|
140
184
|
getRules(): Rule[];
|
|
141
185
|
listRules(): Array<{
|
|
142
186
|
id: string;
|
|
@@ -160,7 +204,7 @@ declare class DiffParser {
|
|
|
160
204
|
private parseHunks;
|
|
161
205
|
}
|
|
162
206
|
|
|
163
|
-
declare function calculateDimensionScore(issues:
|
|
207
|
+
declare function calculateDimensionScore(issues: ReportIssue[]): DimensionScore;
|
|
164
208
|
declare function calculateOverallScore(dimensions: Record<RuleCategory, DimensionScore>, weights: DimensionWeights): number;
|
|
165
209
|
declare function getGrade(score: number): TrustGrade;
|
|
166
210
|
declare function getGradeEmoji(grade: TrustGrade): string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/core/engine.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { readFile } from "fs/promises";
|
|
3
|
-
import { readFileSync
|
|
4
|
-
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
|
|
5
6
|
import { fileURLToPath } from "url";
|
|
6
7
|
|
|
7
8
|
// src/parsers/diff.ts
|
|
@@ -468,7 +469,7 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
471
|
-
const endsOpen = /[{(
|
|
472
|
+
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
472
473
|
if (endsOpen) continue;
|
|
473
474
|
lastReturnDepth = braceDepth;
|
|
474
475
|
lastReturnLine = i;
|
|
@@ -903,6 +904,10 @@ function truncate(s, maxLen) {
|
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
// src/rules/builtin/security.ts
|
|
907
|
+
var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
|
|
908
|
+
var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
|
|
909
|
+
var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
|
|
910
|
+
var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
|
|
906
911
|
var securityRules = [
|
|
907
912
|
{
|
|
908
913
|
id: "security/hardcoded-secret",
|
|
@@ -915,7 +920,7 @@ var securityRules = [
|
|
|
915
920
|
const lines = context.fileContent.split("\n");
|
|
916
921
|
const secretPatterns = [
|
|
917
922
|
// API keys / tokens
|
|
918
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_
|
|
923
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
|
|
919
924
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
920
925
|
// AWS
|
|
921
926
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -968,15 +973,17 @@ var securityRules = [
|
|
|
968
973
|
const issues = [];
|
|
969
974
|
const lines = context.fileContent.split("\n");
|
|
970
975
|
for (let i = 0; i < lines.length; i++) {
|
|
971
|
-
const
|
|
972
|
-
|
|
976
|
+
const line = lines[i];
|
|
977
|
+
const trimmed = line.trim();
|
|
978
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
979
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
973
980
|
const evalPatterns = [
|
|
974
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
975
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
976
|
-
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
|
|
981
|
+
{ pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
|
|
982
|
+
{ pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
|
|
983
|
+
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
|
|
977
984
|
];
|
|
978
|
-
for (const { pattern, label } of evalPatterns) {
|
|
979
|
-
if (pattern.test(
|
|
985
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
986
|
+
if (pattern.test(source)) {
|
|
980
987
|
issues.push({
|
|
981
988
|
ruleId: "security/eval-usage",
|
|
982
989
|
severity: "high",
|
|
@@ -1009,29 +1016,30 @@ var securityRules = [
|
|
|
1009
1016
|
check(context) {
|
|
1010
1017
|
const issues = [];
|
|
1011
1018
|
const lines = context.fileContent.split("\n");
|
|
1019
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
1012
1020
|
for (let i = 0; i < lines.length; i++) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1021
|
+
const line = lines[i];
|
|
1022
|
+
const trimmed = line.trim();
|
|
1023
|
+
if (isCommentLine(trimmed)) continue;
|
|
1024
|
+
const hasSqlKeyword = sqlKeywords.test(line);
|
|
1025
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
|
|
1026
|
+
if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
|
|
1027
|
+
issues.push({
|
|
1028
|
+
ruleId: "security/sql-injection",
|
|
1029
|
+
severity: "high",
|
|
1030
|
+
category: "security",
|
|
1031
|
+
file: context.filePath,
|
|
1032
|
+
startLine: i + 1,
|
|
1033
|
+
endLine: i + 1,
|
|
1034
|
+
message: t(
|
|
1035
|
+
"Potential SQL injection \u2014 string interpolation in SQL query.",
|
|
1036
|
+
"\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
|
|
1037
|
+
),
|
|
1038
|
+
suggestion: t(
|
|
1039
|
+
"Use parameterized queries or prepared statements instead.",
|
|
1040
|
+
"\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
|
|
1041
|
+
)
|
|
1042
|
+
});
|
|
1035
1043
|
}
|
|
1036
1044
|
}
|
|
1037
1045
|
return issues;
|
|
@@ -2315,12 +2323,7 @@ var noReassignParamRule = {
|
|
|
2315
2323
|
}
|
|
2316
2324
|
}
|
|
2317
2325
|
if (paramNames.size === 0) return;
|
|
2318
|
-
|
|
2319
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2320
|
-
body = node.value;
|
|
2321
|
-
} else {
|
|
2322
|
-
body = node;
|
|
2323
|
-
}
|
|
2326
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2324
2327
|
if (!body || !("body" in body)) return;
|
|
2325
2328
|
const fnBody = body.body;
|
|
2326
2329
|
if (!fnBody) return;
|
|
@@ -2557,15 +2560,33 @@ var RuleEngine = class {
|
|
|
2557
2560
|
);
|
|
2558
2561
|
}
|
|
2559
2562
|
run(context) {
|
|
2563
|
+
return this.runWithDiagnostics(context).issues;
|
|
2564
|
+
}
|
|
2565
|
+
runWithDiagnostics(context) {
|
|
2560
2566
|
const allIssues = [];
|
|
2567
|
+
const ruleFailures = [];
|
|
2568
|
+
let rulesExecuted = 0;
|
|
2569
|
+
let rulesFailed = 0;
|
|
2561
2570
|
for (const rule of this.rules) {
|
|
2571
|
+
rulesExecuted++;
|
|
2562
2572
|
try {
|
|
2563
2573
|
const issues = rule.check(context);
|
|
2564
2574
|
allIssues.push(...issues);
|
|
2565
|
-
} catch (
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
rulesFailed++;
|
|
2577
|
+
ruleFailures.push({
|
|
2578
|
+
ruleId: rule.id,
|
|
2579
|
+
file: context.filePath,
|
|
2580
|
+
message: err instanceof Error ? err.message : "Unknown rule execution failure"
|
|
2581
|
+
});
|
|
2566
2582
|
}
|
|
2567
2583
|
}
|
|
2568
|
-
return
|
|
2584
|
+
return {
|
|
2585
|
+
issues: allIssues,
|
|
2586
|
+
rulesExecuted,
|
|
2587
|
+
rulesFailed,
|
|
2588
|
+
ruleFailures
|
|
2589
|
+
};
|
|
2569
2590
|
}
|
|
2570
2591
|
getRules() {
|
|
2571
2592
|
return [...this.rules];
|
|
@@ -2945,6 +2966,8 @@ var PKG_VERSION = (() => {
|
|
|
2945
2966
|
return "0.1.0";
|
|
2946
2967
|
}
|
|
2947
2968
|
})();
|
|
2969
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
2970
|
+
var FINGERPRINT_VERSION = "1";
|
|
2948
2971
|
var ScanEngine = class {
|
|
2949
2972
|
config;
|
|
2950
2973
|
diffParser;
|
|
@@ -2955,80 +2978,198 @@ var ScanEngine = class {
|
|
|
2955
2978
|
this.ruleEngine = new RuleEngine(config);
|
|
2956
2979
|
}
|
|
2957
2980
|
async scan(options) {
|
|
2958
|
-
const
|
|
2981
|
+
const selection = await this.getScanCandidates(options);
|
|
2959
2982
|
const allIssues = [];
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
if (line.startsWith("+")) {
|
|
2981
|
-
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
2982
|
-
currentLine++;
|
|
2983
|
-
} else if (line.startsWith("-")) {
|
|
2984
|
-
} else {
|
|
2985
|
-
currentLine++;
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
return result;
|
|
2989
|
-
});
|
|
2990
|
-
const issues = this.ruleEngine.run({
|
|
2991
|
-
filePath: diffFile.filePath,
|
|
2992
|
-
fileContent,
|
|
2993
|
-
addedLines
|
|
2994
|
-
});
|
|
2995
|
-
allIssues.push(...issues);
|
|
2996
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
2997
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
2998
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
2999
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3000
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3001
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3002
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
3003
|
-
});
|
|
3004
|
-
allIssues.push(...structureResult.issues);
|
|
3005
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3006
|
-
allIssues.push(...styleResult.issues);
|
|
3007
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3008
|
-
allIssues.push(...coverageResult.issues);
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
const dimensions = this.groupByDimension(allIssues);
|
|
2983
|
+
const scanErrors = [];
|
|
2984
|
+
const ruleFailures = [];
|
|
2985
|
+
let rulesExecuted = 0;
|
|
2986
|
+
let rulesFailed = 0;
|
|
2987
|
+
let filesScanned = 0;
|
|
2988
|
+
const results = await Promise.all(
|
|
2989
|
+
selection.candidates.map((diffFile) => this.scanFile(diffFile))
|
|
2990
|
+
);
|
|
2991
|
+
for (const result of results) {
|
|
2992
|
+
allIssues.push(...result.issues);
|
|
2993
|
+
ruleFailures.push(...result.ruleFailures);
|
|
2994
|
+
scanErrors.push(...result.scanErrors);
|
|
2995
|
+
rulesExecuted += result.rulesExecuted;
|
|
2996
|
+
rulesFailed += result.rulesFailed;
|
|
2997
|
+
if (result.scanned) {
|
|
2998
|
+
filesScanned++;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3002
|
+
const dimensions = this.groupByDimension(issuesWithFingerprints);
|
|
3012
3003
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3013
3004
|
const grade = getGrade(overallScore);
|
|
3014
3005
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
3015
3006
|
return {
|
|
3007
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
3016
3008
|
version: PKG_VERSION,
|
|
3017
3009
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3018
3010
|
commit: commitHash,
|
|
3011
|
+
scanMode: selection.scanMode,
|
|
3019
3012
|
overall: {
|
|
3020
3013
|
score: overallScore,
|
|
3021
3014
|
grade,
|
|
3022
|
-
filesScanned
|
|
3023
|
-
issuesFound:
|
|
3015
|
+
filesScanned,
|
|
3016
|
+
issuesFound: issuesWithFingerprints.length
|
|
3017
|
+
},
|
|
3018
|
+
toolHealth: {
|
|
3019
|
+
rulesExecuted,
|
|
3020
|
+
rulesFailed,
|
|
3021
|
+
filesConsidered: selection.filesConsidered,
|
|
3022
|
+
filesScanned,
|
|
3023
|
+
filesExcluded: selection.filesExcluded,
|
|
3024
|
+
filesSkipped: scanErrors.length,
|
|
3025
|
+
scanErrors,
|
|
3026
|
+
ruleFailures
|
|
3024
3027
|
},
|
|
3025
3028
|
dimensions,
|
|
3026
|
-
issues:
|
|
3029
|
+
issues: issuesWithFingerprints.sort((a, b) => {
|
|
3027
3030
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3028
3031
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3029
3032
|
})
|
|
3030
3033
|
};
|
|
3031
3034
|
}
|
|
3035
|
+
async scanFile(diffFile) {
|
|
3036
|
+
if (diffFile.status === "deleted") {
|
|
3037
|
+
return {
|
|
3038
|
+
issues: [],
|
|
3039
|
+
ruleFailures: [],
|
|
3040
|
+
rulesExecuted: 0,
|
|
3041
|
+
rulesFailed: 0,
|
|
3042
|
+
scanErrors: [
|
|
3043
|
+
{
|
|
3044
|
+
type: "deleted-file",
|
|
3045
|
+
file: diffFile.filePath,
|
|
3046
|
+
message: `Skipped deleted file: ${diffFile.filePath}`
|
|
3047
|
+
}
|
|
3048
|
+
],
|
|
3049
|
+
scanned: false
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
if (!this.isTsJsFile(diffFile.filePath)) {
|
|
3053
|
+
return {
|
|
3054
|
+
issues: [],
|
|
3055
|
+
ruleFailures: [],
|
|
3056
|
+
rulesExecuted: 0,
|
|
3057
|
+
rulesFailed: 0,
|
|
3058
|
+
scanErrors: [
|
|
3059
|
+
{
|
|
3060
|
+
type: "unsupported-file-type",
|
|
3061
|
+
file: diffFile.filePath,
|
|
3062
|
+
message: `Skipped unsupported file type: ${diffFile.filePath}`
|
|
3063
|
+
}
|
|
3064
|
+
],
|
|
3065
|
+
scanned: false
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
const filePath = resolve2(diffFile.filePath);
|
|
3069
|
+
let fileContent;
|
|
3070
|
+
try {
|
|
3071
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
3072
|
+
} catch {
|
|
3073
|
+
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3074
|
+
if (!content) {
|
|
3075
|
+
return {
|
|
3076
|
+
issues: [],
|
|
3077
|
+
ruleFailures: [],
|
|
3078
|
+
rulesExecuted: 0,
|
|
3079
|
+
rulesFailed: 0,
|
|
3080
|
+
scanErrors: [
|
|
3081
|
+
{
|
|
3082
|
+
type: "missing-file-content",
|
|
3083
|
+
file: diffFile.filePath,
|
|
3084
|
+
message: `Unable to read file content for ${diffFile.filePath}`
|
|
3085
|
+
}
|
|
3086
|
+
],
|
|
3087
|
+
scanned: false
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
fileContent = content;
|
|
3091
|
+
}
|
|
3092
|
+
const addedLines = diffFile.hunks.flatMap((hunk) => {
|
|
3093
|
+
const lines = hunk.content.split("\n");
|
|
3094
|
+
const result = [];
|
|
3095
|
+
let currentLine = hunk.newStart;
|
|
3096
|
+
for (const line of lines) {
|
|
3097
|
+
if (line.startsWith("+")) {
|
|
3098
|
+
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3099
|
+
currentLine++;
|
|
3100
|
+
} else if (line.startsWith("-")) {
|
|
3101
|
+
} else {
|
|
3102
|
+
currentLine++;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
return result;
|
|
3106
|
+
});
|
|
3107
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3108
|
+
filePath: diffFile.filePath,
|
|
3109
|
+
fileContent,
|
|
3110
|
+
addedLines
|
|
3111
|
+
});
|
|
3112
|
+
const issues = [...ruleResult.issues];
|
|
3113
|
+
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3114
|
+
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3115
|
+
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3116
|
+
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3117
|
+
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3118
|
+
maxParamCount: this.config.thresholds["max-params"]
|
|
3119
|
+
});
|
|
3120
|
+
issues.push(...structureResult.issues);
|
|
3121
|
+
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3122
|
+
issues.push(...styleResult.issues);
|
|
3123
|
+
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3124
|
+
issues.push(...coverageResult.issues);
|
|
3125
|
+
return {
|
|
3126
|
+
issues,
|
|
3127
|
+
ruleFailures: ruleResult.ruleFailures,
|
|
3128
|
+
rulesExecuted: ruleResult.rulesExecuted,
|
|
3129
|
+
rulesFailed: ruleResult.rulesFailed,
|
|
3130
|
+
scanErrors: [],
|
|
3131
|
+
scanned: true
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
async getScanCandidates(options) {
|
|
3135
|
+
const scanMode = this.getScanMode(options);
|
|
3136
|
+
const candidates = await this.getDiffFiles(options);
|
|
3137
|
+
if (scanMode === "files") {
|
|
3138
|
+
return {
|
|
3139
|
+
scanMode,
|
|
3140
|
+
candidates,
|
|
3141
|
+
filesConsidered: options.files?.length ?? candidates.length,
|
|
3142
|
+
filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
const filteredCandidates = [];
|
|
3146
|
+
let filesExcluded = 0;
|
|
3147
|
+
for (const candidate of candidates) {
|
|
3148
|
+
if (this.shouldIncludeFile(candidate.filePath)) {
|
|
3149
|
+
filteredCandidates.push(candidate);
|
|
3150
|
+
} else {
|
|
3151
|
+
filesExcluded++;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return {
|
|
3155
|
+
scanMode,
|
|
3156
|
+
candidates: filteredCandidates,
|
|
3157
|
+
filesConsidered: candidates.length,
|
|
3158
|
+
filesExcluded
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
getScanMode(options) {
|
|
3162
|
+
if (options.staged) {
|
|
3163
|
+
return "staged";
|
|
3164
|
+
}
|
|
3165
|
+
if (options.diff) {
|
|
3166
|
+
return "diff";
|
|
3167
|
+
}
|
|
3168
|
+
if (options.files && options.files.length > 0) {
|
|
3169
|
+
return "files";
|
|
3170
|
+
}
|
|
3171
|
+
return "changed";
|
|
3172
|
+
}
|
|
3032
3173
|
async getDiffFiles(options) {
|
|
3033
3174
|
if (options.staged) {
|
|
3034
3175
|
return this.diffParser.getStagedFiles();
|
|
@@ -3037,14 +3178,10 @@ var ScanEngine = class {
|
|
|
3037
3178
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
3038
3179
|
}
|
|
3039
3180
|
if (options.files && options.files.length > 0) {
|
|
3181
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
3040
3182
|
return Promise.all(
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
try {
|
|
3044
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
3045
|
-
} catch {
|
|
3046
|
-
}
|
|
3047
|
-
return {
|
|
3183
|
+
includedFiles.map(
|
|
3184
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
3048
3185
|
filePath,
|
|
3049
3186
|
status: "modified",
|
|
3050
3187
|
additions: content.split("\n").length,
|
|
@@ -3056,18 +3193,64 @@ var ScanEngine = class {
|
|
|
3056
3193
|
oldLines: 0,
|
|
3057
3194
|
newStart: 1,
|
|
3058
3195
|
newLines: content.split("\n").length,
|
|
3059
|
-
content: content.split("\n").map((
|
|
3196
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
3060
3197
|
}
|
|
3061
3198
|
]
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3199
|
+
}))
|
|
3200
|
+
)
|
|
3064
3201
|
);
|
|
3065
3202
|
}
|
|
3066
3203
|
return this.diffParser.getChangedFiles();
|
|
3067
3204
|
}
|
|
3205
|
+
shouldIncludeFile(filePath) {
|
|
3206
|
+
const normalizedPath = filePath.split(sep).join("/");
|
|
3207
|
+
const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
|
|
3208
|
+
const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3209
|
+
if (!included) {
|
|
3210
|
+
return false;
|
|
3211
|
+
}
|
|
3212
|
+
return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3213
|
+
}
|
|
3214
|
+
matchesPattern(filePath, pattern) {
|
|
3215
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3216
|
+
regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
|
|
3217
|
+
regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
3218
|
+
regexPattern = regexPattern.replace(/\*/g, "[^/]*");
|
|
3219
|
+
regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
|
|
3220
|
+
regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
|
|
3221
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
3222
|
+
}
|
|
3068
3223
|
isTsJsFile(filePath) {
|
|
3069
3224
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
3070
3225
|
}
|
|
3226
|
+
attachFingerprints(issues) {
|
|
3227
|
+
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3228
|
+
return issues.map((issue) => {
|
|
3229
|
+
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3230
|
+
const locationComponent = `${issue.startLine}:${issue.endLine}`;
|
|
3231
|
+
const baseKey = [
|
|
3232
|
+
issue.ruleId,
|
|
3233
|
+
normalizedFile,
|
|
3234
|
+
issue.category,
|
|
3235
|
+
issue.severity,
|
|
3236
|
+
locationComponent
|
|
3237
|
+
].join("|");
|
|
3238
|
+
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3239
|
+
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
3240
|
+
const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
|
|
3241
|
+
return {
|
|
3242
|
+
...issue,
|
|
3243
|
+
file: normalizedFile,
|
|
3244
|
+
fingerprint,
|
|
3245
|
+
fingerprintVersion: FINGERPRINT_VERSION
|
|
3246
|
+
};
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
normalizeRelativePath(filePath) {
|
|
3250
|
+
const absolutePath = resolve2(filePath);
|
|
3251
|
+
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3252
|
+
return relativePath.split(sep).join("/");
|
|
3253
|
+
}
|
|
3071
3254
|
groupByDimension(issues) {
|
|
3072
3255
|
const categories = [
|
|
3073
3256
|
"security",
|
|
@@ -3078,7 +3261,7 @@ var ScanEngine = class {
|
|
|
3078
3261
|
];
|
|
3079
3262
|
const grouped = {};
|
|
3080
3263
|
for (const cat of categories) {
|
|
3081
|
-
const catIssues = issues.filter((
|
|
3264
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3082
3265
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3083
3266
|
}
|
|
3084
3267
|
return grouped;
|