@gulu9527/code-trust 0.2.0 → 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 +51 -4
- package/dist/cli/index.js +644 -129
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +56 -2
- package/dist/index.js +372 -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.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;
|
|
@@ -524,6 +525,24 @@ function detectImmediateReassign(context, lines, issues) {
|
|
|
524
525
|
}
|
|
525
526
|
}
|
|
526
527
|
|
|
528
|
+
// src/rules/fix-utils.ts
|
|
529
|
+
function lineStartOffset(content, lineNumber) {
|
|
530
|
+
let offset = 0;
|
|
531
|
+
const lines = content.split("\n");
|
|
532
|
+
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
533
|
+
offset += lines[i].length + 1;
|
|
534
|
+
}
|
|
535
|
+
return offset;
|
|
536
|
+
}
|
|
537
|
+
function lineRange(content, lineNumber) {
|
|
538
|
+
const lines = content.split("\n");
|
|
539
|
+
const lineIndex = lineNumber - 1;
|
|
540
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
|
|
541
|
+
const start = lineStartOffset(content, lineNumber);
|
|
542
|
+
const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
|
|
543
|
+
return [start, end];
|
|
544
|
+
}
|
|
545
|
+
|
|
527
546
|
// src/parsers/ast.ts
|
|
528
547
|
import { parse, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree";
|
|
529
548
|
|
|
@@ -706,6 +725,19 @@ var unusedVariablesRule = {
|
|
|
706
725
|
severity: "low",
|
|
707
726
|
title: "Unused variable detected",
|
|
708
727
|
description: "AI-generated code sometimes declares variables that are never used, indicating incomplete or hallucinated logic.",
|
|
728
|
+
fixable: true,
|
|
729
|
+
fix(context, issue) {
|
|
730
|
+
const lines = context.fileContent.split("\n");
|
|
731
|
+
const lineIndex = issue.startLine - 1;
|
|
732
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
733
|
+
const line = lines[lineIndex].trim();
|
|
734
|
+
if (/^(const|let|var)\s+\w+\s*[=:;]/.test(line) && !line.includes(",")) {
|
|
735
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
736
|
+
if (start === end) return null;
|
|
737
|
+
return { range: [start, end], text: "" };
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
},
|
|
709
741
|
check(context) {
|
|
710
742
|
const issues = [];
|
|
711
743
|
let ast;
|
|
@@ -872,6 +904,10 @@ function truncate(s, maxLen) {
|
|
|
872
904
|
}
|
|
873
905
|
|
|
874
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);
|
|
875
911
|
var securityRules = [
|
|
876
912
|
{
|
|
877
913
|
id: "security/hardcoded-secret",
|
|
@@ -884,7 +920,7 @@ var securityRules = [
|
|
|
884
920
|
const lines = context.fileContent.split("\n");
|
|
885
921
|
const secretPatterns = [
|
|
886
922
|
// API keys / tokens
|
|
887
|
-
{ 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" },
|
|
888
924
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
889
925
|
// AWS
|
|
890
926
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -937,15 +973,17 @@ var securityRules = [
|
|
|
937
973
|
const issues = [];
|
|
938
974
|
const lines = context.fileContent.split("\n");
|
|
939
975
|
for (let i = 0; i < lines.length; i++) {
|
|
940
|
-
const
|
|
941
|
-
|
|
976
|
+
const line = lines[i];
|
|
977
|
+
const trimmed = line.trim();
|
|
978
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
979
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
942
980
|
const evalPatterns = [
|
|
943
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
944
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
945
|
-
{ 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 }
|
|
946
984
|
];
|
|
947
|
-
for (const { pattern, label } of evalPatterns) {
|
|
948
|
-
if (pattern.test(
|
|
985
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
986
|
+
if (pattern.test(source)) {
|
|
949
987
|
issues.push({
|
|
950
988
|
ruleId: "security/eval-usage",
|
|
951
989
|
severity: "high",
|
|
@@ -978,29 +1016,30 @@ var securityRules = [
|
|
|
978
1016
|
check(context) {
|
|
979
1017
|
const issues = [];
|
|
980
1018
|
const lines = context.fileContent.split("\n");
|
|
1019
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
981
1020
|
for (let i = 0; i < lines.length; i++) {
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
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
|
+
});
|
|
1004
1043
|
}
|
|
1005
1044
|
}
|
|
1006
1045
|
return issues;
|
|
@@ -1429,6 +1468,23 @@ var unusedImportRule = {
|
|
|
1429
1468
|
severity: "low",
|
|
1430
1469
|
title: "Unused import",
|
|
1431
1470
|
description: "AI-generated code often imports modules or identifiers that are never used in the file.",
|
|
1471
|
+
fixable: true,
|
|
1472
|
+
fix(context, issue) {
|
|
1473
|
+
const lines = context.fileContent.split("\n");
|
|
1474
|
+
const lineIndex = issue.startLine - 1;
|
|
1475
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1476
|
+
const line = lines[lineIndex].trim();
|
|
1477
|
+
const isSingleDefault = /^import\s+\w+\s+from\s+/.test(line);
|
|
1478
|
+
const isSingleNamed = /^import\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1479
|
+
const isSingleTypeNamed = /^import\s+type\s*\{\s*\w+\s*\}\s*from\s+/.test(line);
|
|
1480
|
+
const isSingleTypeDefault = /^import\s+type\s+\w+\s+from\s+/.test(line);
|
|
1481
|
+
if (!isSingleDefault && !isSingleNamed && !isSingleTypeNamed && !isSingleTypeDefault) {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
1485
|
+
if (start === end) return null;
|
|
1486
|
+
return { range: [start, end], text: "" };
|
|
1487
|
+
},
|
|
1432
1488
|
check(context) {
|
|
1433
1489
|
const issues = [];
|
|
1434
1490
|
let ast;
|
|
@@ -1666,6 +1722,33 @@ var typeCoercionRule = {
|
|
|
1666
1722
|
severity: "medium",
|
|
1667
1723
|
title: "Loose equality with type coercion",
|
|
1668
1724
|
description: "AI-generated code often uses == instead of ===, leading to implicit type coercion bugs.",
|
|
1725
|
+
fixable: true,
|
|
1726
|
+
fix(context, issue) {
|
|
1727
|
+
const lines = context.fileContent.split("\n");
|
|
1728
|
+
const lineIndex = issue.startLine - 1;
|
|
1729
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
|
1730
|
+
const line = lines[lineIndex];
|
|
1731
|
+
const base = lineStartOffset(context.fileContent, issue.startLine);
|
|
1732
|
+
const isNotEqual = issue.message.includes("!=");
|
|
1733
|
+
const searchOp = isNotEqual ? "!=" : "==";
|
|
1734
|
+
const replaceOp = isNotEqual ? "!==" : "===";
|
|
1735
|
+
let pos = -1;
|
|
1736
|
+
for (let j = 0; j < line.length - 1; j++) {
|
|
1737
|
+
if (line[j] === searchOp[0] && line[j + 1] === "=") {
|
|
1738
|
+
if (line[j + 2] === "=") {
|
|
1739
|
+
j += 2;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (!isNotEqual && j > 0 && (line[j - 1] === "!" || line[j - 1] === "<" || line[j - 1] === ">")) {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
pos = j;
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (pos === -1) return null;
|
|
1750
|
+
return { range: [base + pos, base + pos + searchOp.length], text: replaceOp };
|
|
1751
|
+
},
|
|
1669
1752
|
check(context) {
|
|
1670
1753
|
const issues = [];
|
|
1671
1754
|
const lines = context.fileContent.split("\n");
|
|
@@ -1920,6 +2003,12 @@ var noDebuggerRule = {
|
|
|
1920
2003
|
severity: "high",
|
|
1921
2004
|
title: "Debugger statement",
|
|
1922
2005
|
description: "Debugger statements should never be committed to production code.",
|
|
2006
|
+
fixable: true,
|
|
2007
|
+
fix(context, issue) {
|
|
2008
|
+
const [start, end] = lineRange(context.fileContent, issue.startLine);
|
|
2009
|
+
if (start === end) return null;
|
|
2010
|
+
return { range: [start, end], text: "" };
|
|
2011
|
+
},
|
|
1923
2012
|
check(context) {
|
|
1924
2013
|
const issues = [];
|
|
1925
2014
|
const lines = context.fileContent.split("\n");
|
|
@@ -2234,12 +2323,7 @@ var noReassignParamRule = {
|
|
|
2234
2323
|
}
|
|
2235
2324
|
}
|
|
2236
2325
|
if (paramNames.size === 0) return;
|
|
2237
|
-
|
|
2238
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2239
|
-
body = node.value;
|
|
2240
|
-
} else {
|
|
2241
|
-
body = node;
|
|
2242
|
-
}
|
|
2326
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2243
2327
|
if (!body || !("body" in body)) return;
|
|
2244
2328
|
const fnBody = body.body;
|
|
2245
2329
|
if (!fnBody) return;
|
|
@@ -2476,15 +2560,33 @@ var RuleEngine = class {
|
|
|
2476
2560
|
);
|
|
2477
2561
|
}
|
|
2478
2562
|
run(context) {
|
|
2563
|
+
return this.runWithDiagnostics(context).issues;
|
|
2564
|
+
}
|
|
2565
|
+
runWithDiagnostics(context) {
|
|
2479
2566
|
const allIssues = [];
|
|
2567
|
+
const ruleFailures = [];
|
|
2568
|
+
let rulesExecuted = 0;
|
|
2569
|
+
let rulesFailed = 0;
|
|
2480
2570
|
for (const rule of this.rules) {
|
|
2571
|
+
rulesExecuted++;
|
|
2481
2572
|
try {
|
|
2482
2573
|
const issues = rule.check(context);
|
|
2483
2574
|
allIssues.push(...issues);
|
|
2484
|
-
} 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
|
+
});
|
|
2485
2582
|
}
|
|
2486
2583
|
}
|
|
2487
|
-
return
|
|
2584
|
+
return {
|
|
2585
|
+
issues: allIssues,
|
|
2586
|
+
rulesExecuted,
|
|
2587
|
+
rulesFailed,
|
|
2588
|
+
ruleFailures
|
|
2589
|
+
};
|
|
2488
2590
|
}
|
|
2489
2591
|
getRules() {
|
|
2490
2592
|
return [...this.rules];
|
|
@@ -2864,6 +2966,8 @@ var PKG_VERSION = (() => {
|
|
|
2864
2966
|
return "0.1.0";
|
|
2865
2967
|
}
|
|
2866
2968
|
})();
|
|
2969
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
2970
|
+
var FINGERPRINT_VERSION = "1";
|
|
2867
2971
|
var ScanEngine = class {
|
|
2868
2972
|
config;
|
|
2869
2973
|
diffParser;
|
|
@@ -2874,80 +2978,198 @@ var ScanEngine = class {
|
|
|
2874
2978
|
this.ruleEngine = new RuleEngine(config);
|
|
2875
2979
|
}
|
|
2876
2980
|
async scan(options) {
|
|
2877
|
-
const
|
|
2981
|
+
const selection = await this.getScanCandidates(options);
|
|
2878
2982
|
const allIssues = [];
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
if (line.startsWith("+")) {
|
|
2900
|
-
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
2901
|
-
currentLine++;
|
|
2902
|
-
} else if (line.startsWith("-")) {
|
|
2903
|
-
} else {
|
|
2904
|
-
currentLine++;
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
return result;
|
|
2908
|
-
});
|
|
2909
|
-
const issues = this.ruleEngine.run({
|
|
2910
|
-
filePath: diffFile.filePath,
|
|
2911
|
-
fileContent,
|
|
2912
|
-
addedLines
|
|
2913
|
-
});
|
|
2914
|
-
allIssues.push(...issues);
|
|
2915
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
2916
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
2917
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
2918
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
2919
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
2920
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
2921
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
2922
|
-
});
|
|
2923
|
-
allIssues.push(...structureResult.issues);
|
|
2924
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
2925
|
-
allIssues.push(...styleResult.issues);
|
|
2926
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
2927
|
-
allIssues.push(...coverageResult.issues);
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
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);
|
|
2931
3003
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
2932
3004
|
const grade = getGrade(overallScore);
|
|
2933
3005
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
2934
3006
|
return {
|
|
3007
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
2935
3008
|
version: PKG_VERSION,
|
|
2936
3009
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2937
3010
|
commit: commitHash,
|
|
3011
|
+
scanMode: selection.scanMode,
|
|
2938
3012
|
overall: {
|
|
2939
3013
|
score: overallScore,
|
|
2940
3014
|
grade,
|
|
2941
|
-
filesScanned
|
|
2942
|
-
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
|
|
2943
3027
|
},
|
|
2944
3028
|
dimensions,
|
|
2945
|
-
issues:
|
|
3029
|
+
issues: issuesWithFingerprints.sort((a, b) => {
|
|
2946
3030
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
2947
3031
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
2948
3032
|
})
|
|
2949
3033
|
};
|
|
2950
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
|
+
}
|
|
2951
3173
|
async getDiffFiles(options) {
|
|
2952
3174
|
if (options.staged) {
|
|
2953
3175
|
return this.diffParser.getStagedFiles();
|
|
@@ -2956,14 +3178,10 @@ var ScanEngine = class {
|
|
|
2956
3178
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
2957
3179
|
}
|
|
2958
3180
|
if (options.files && options.files.length > 0) {
|
|
3181
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
2959
3182
|
return Promise.all(
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
try {
|
|
2963
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
2964
|
-
} catch {
|
|
2965
|
-
}
|
|
2966
|
-
return {
|
|
3183
|
+
includedFiles.map(
|
|
3184
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
2967
3185
|
filePath,
|
|
2968
3186
|
status: "modified",
|
|
2969
3187
|
additions: content.split("\n").length,
|
|
@@ -2975,18 +3193,64 @@ var ScanEngine = class {
|
|
|
2975
3193
|
oldLines: 0,
|
|
2976
3194
|
newStart: 1,
|
|
2977
3195
|
newLines: content.split("\n").length,
|
|
2978
|
-
content: content.split("\n").map((
|
|
3196
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
2979
3197
|
}
|
|
2980
3198
|
]
|
|
2981
|
-
}
|
|
2982
|
-
|
|
3199
|
+
}))
|
|
3200
|
+
)
|
|
2983
3201
|
);
|
|
2984
3202
|
}
|
|
2985
3203
|
return this.diffParser.getChangedFiles();
|
|
2986
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
|
+
}
|
|
2987
3223
|
isTsJsFile(filePath) {
|
|
2988
3224
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
2989
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
|
+
}
|
|
2990
3254
|
groupByDimension(issues) {
|
|
2991
3255
|
const categories = [
|
|
2992
3256
|
"security",
|
|
@@ -2997,7 +3261,7 @@ var ScanEngine = class {
|
|
|
2997
3261
|
];
|
|
2998
3262
|
const grouped = {};
|
|
2999
3263
|
for (const cat of categories) {
|
|
3000
|
-
const catIssues = issues.filter((
|
|
3264
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3001
3265
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3002
3266
|
}
|
|
3003
3267
|
return grouped;
|