@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/cli/index.js
CHANGED
|
@@ -120,9 +120,10 @@ detection:
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// src/core/engine.ts
|
|
123
|
+
import { createHash } from "crypto";
|
|
123
124
|
import { readFile } from "fs/promises";
|
|
124
|
-
import { readFileSync
|
|
125
|
-
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
125
|
+
import { readFileSync } from "fs";
|
|
126
|
+
import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
|
|
126
127
|
import { fileURLToPath } from "url";
|
|
127
128
|
|
|
128
129
|
// src/parsers/diff.ts
|
|
@@ -589,7 +590,7 @@ function detectCodeAfterReturn(context, lines, issues) {
|
|
|
589
590
|
}
|
|
590
591
|
}
|
|
591
592
|
if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
|
|
592
|
-
const endsOpen = /[{(
|
|
593
|
+
const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
|
|
593
594
|
if (endsOpen) continue;
|
|
594
595
|
lastReturnDepth = braceDepth;
|
|
595
596
|
lastReturnLine = i;
|
|
@@ -1024,6 +1025,10 @@ function truncate(s, maxLen) {
|
|
|
1024
1025
|
}
|
|
1025
1026
|
|
|
1026
1027
|
// src/rules/builtin/security.ts
|
|
1028
|
+
var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
|
|
1029
|
+
var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
|
|
1030
|
+
var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
|
|
1031
|
+
var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
|
|
1027
1032
|
var securityRules = [
|
|
1028
1033
|
{
|
|
1029
1034
|
id: "security/hardcoded-secret",
|
|
@@ -1036,7 +1041,7 @@ var securityRules = [
|
|
|
1036
1041
|
const lines = context.fileContent.split("\n");
|
|
1037
1042
|
const secretPatterns = [
|
|
1038
1043
|
// API keys / tokens
|
|
1039
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_
|
|
1044
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
|
|
1040
1045
|
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
|
|
1041
1046
|
// AWS
|
|
1042
1047
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
@@ -1089,15 +1094,17 @@ var securityRules = [
|
|
|
1089
1094
|
const issues = [];
|
|
1090
1095
|
const lines = context.fileContent.split("\n");
|
|
1091
1096
|
for (let i = 0; i < lines.length; i++) {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1097
|
+
const line = lines[i];
|
|
1098
|
+
const trimmed = line.trim();
|
|
1099
|
+
if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
|
|
1100
|
+
const sanitizedLine = stripQuotedStrings(line);
|
|
1094
1101
|
const evalPatterns = [
|
|
1095
|
-
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
1096
|
-
{ pattern: /new\s+Function\s*\(/, label: "new Function()" },
|
|
1097
|
-
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
|
|
1102
|
+
{ pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
|
|
1103
|
+
{ pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
|
|
1104
|
+
{ pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
|
|
1098
1105
|
];
|
|
1099
|
-
for (const { pattern, label } of evalPatterns) {
|
|
1100
|
-
if (pattern.test(
|
|
1106
|
+
for (const { pattern, label, source } of evalPatterns) {
|
|
1107
|
+
if (pattern.test(source)) {
|
|
1101
1108
|
issues.push({
|
|
1102
1109
|
ruleId: "security/eval-usage",
|
|
1103
1110
|
severity: "high",
|
|
@@ -1130,29 +1137,30 @@ var securityRules = [
|
|
|
1130
1137
|
check(context) {
|
|
1131
1138
|
const issues = [];
|
|
1132
1139
|
const lines = context.fileContent.split("\n");
|
|
1140
|
+
const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
|
|
1133
1141
|
for (let i = 0; i < lines.length; i++) {
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1142
|
+
const line = lines[i];
|
|
1143
|
+
const trimmed = line.trim();
|
|
1144
|
+
if (isCommentLine(trimmed)) continue;
|
|
1145
|
+
const hasSqlKeyword = sqlKeywords.test(line);
|
|
1146
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
|
|
1147
|
+
if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
|
|
1148
|
+
issues.push({
|
|
1149
|
+
ruleId: "security/sql-injection",
|
|
1150
|
+
severity: "high",
|
|
1151
|
+
category: "security",
|
|
1152
|
+
file: context.filePath,
|
|
1153
|
+
startLine: i + 1,
|
|
1154
|
+
endLine: i + 1,
|
|
1155
|
+
message: t(
|
|
1156
|
+
"Potential SQL injection \u2014 string interpolation in SQL query.",
|
|
1157
|
+
"\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
|
|
1158
|
+
),
|
|
1159
|
+
suggestion: t(
|
|
1160
|
+
"Use parameterized queries or prepared statements instead.",
|
|
1161
|
+
"\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
|
|
1162
|
+
)
|
|
1163
|
+
});
|
|
1156
1164
|
}
|
|
1157
1165
|
}
|
|
1158
1166
|
return issues;
|
|
@@ -2436,12 +2444,7 @@ var noReassignParamRule = {
|
|
|
2436
2444
|
}
|
|
2437
2445
|
}
|
|
2438
2446
|
if (paramNames.size === 0) return;
|
|
2439
|
-
|
|
2440
|
-
if (node.type === AST_NODE_TYPES.MethodDefinition) {
|
|
2441
|
-
body = node.value;
|
|
2442
|
-
} else {
|
|
2443
|
-
body = node;
|
|
2444
|
-
}
|
|
2447
|
+
const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
2445
2448
|
if (!body || !("body" in body)) return;
|
|
2446
2449
|
const fnBody = body.body;
|
|
2447
2450
|
if (!fnBody) return;
|
|
@@ -2678,15 +2681,33 @@ var RuleEngine = class {
|
|
|
2678
2681
|
);
|
|
2679
2682
|
}
|
|
2680
2683
|
run(context) {
|
|
2684
|
+
return this.runWithDiagnostics(context).issues;
|
|
2685
|
+
}
|
|
2686
|
+
runWithDiagnostics(context) {
|
|
2681
2687
|
const allIssues = [];
|
|
2688
|
+
const ruleFailures = [];
|
|
2689
|
+
let rulesExecuted = 0;
|
|
2690
|
+
let rulesFailed = 0;
|
|
2682
2691
|
for (const rule of this.rules) {
|
|
2692
|
+
rulesExecuted++;
|
|
2683
2693
|
try {
|
|
2684
2694
|
const issues = rule.check(context);
|
|
2685
2695
|
allIssues.push(...issues);
|
|
2686
|
-
} catch (
|
|
2696
|
+
} catch (err) {
|
|
2697
|
+
rulesFailed++;
|
|
2698
|
+
ruleFailures.push({
|
|
2699
|
+
ruleId: rule.id,
|
|
2700
|
+
file: context.filePath,
|
|
2701
|
+
message: err instanceof Error ? err.message : "Unknown rule execution failure"
|
|
2702
|
+
});
|
|
2687
2703
|
}
|
|
2688
2704
|
}
|
|
2689
|
-
return
|
|
2705
|
+
return {
|
|
2706
|
+
issues: allIssues,
|
|
2707
|
+
rulesExecuted,
|
|
2708
|
+
rulesFailed,
|
|
2709
|
+
ruleFailures
|
|
2710
|
+
};
|
|
2690
2711
|
}
|
|
2691
2712
|
getRules() {
|
|
2692
2713
|
return [...this.rules];
|
|
@@ -3066,6 +3087,8 @@ var PKG_VERSION = (() => {
|
|
|
3066
3087
|
return "0.1.0";
|
|
3067
3088
|
}
|
|
3068
3089
|
})();
|
|
3090
|
+
var REPORT_SCHEMA_VERSION = "1.0.0";
|
|
3091
|
+
var FINGERPRINT_VERSION = "1";
|
|
3069
3092
|
var ScanEngine = class {
|
|
3070
3093
|
config;
|
|
3071
3094
|
diffParser;
|
|
@@ -3076,80 +3099,198 @@ var ScanEngine = class {
|
|
|
3076
3099
|
this.ruleEngine = new RuleEngine(config);
|
|
3077
3100
|
}
|
|
3078
3101
|
async scan(options) {
|
|
3079
|
-
const
|
|
3102
|
+
const selection = await this.getScanCandidates(options);
|
|
3080
3103
|
const allIssues = [];
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
const lines = hunk.content.split("\n");
|
|
3098
|
-
const result = [];
|
|
3099
|
-
let currentLine = hunk.newStart;
|
|
3100
|
-
for (const line of lines) {
|
|
3101
|
-
if (line.startsWith("+")) {
|
|
3102
|
-
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3103
|
-
currentLine++;
|
|
3104
|
-
} else if (line.startsWith("-")) {
|
|
3105
|
-
} else {
|
|
3106
|
-
currentLine++;
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
return result;
|
|
3110
|
-
});
|
|
3111
|
-
const issues = this.ruleEngine.run({
|
|
3112
|
-
filePath: diffFile.filePath,
|
|
3113
|
-
fileContent,
|
|
3114
|
-
addedLines
|
|
3115
|
-
});
|
|
3116
|
-
allIssues.push(...issues);
|
|
3117
|
-
if (this.isTsJsFile(diffFile.filePath)) {
|
|
3118
|
-
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3119
|
-
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3120
|
-
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3121
|
-
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3122
|
-
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3123
|
-
maxParamCount: this.config.thresholds["max-params"]
|
|
3124
|
-
});
|
|
3125
|
-
allIssues.push(...structureResult.issues);
|
|
3126
|
-
const styleResult = analyzeStyle(fileContent, diffFile.filePath);
|
|
3127
|
-
allIssues.push(...styleResult.issues);
|
|
3128
|
-
const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
|
|
3129
|
-
allIssues.push(...coverageResult.issues);
|
|
3104
|
+
const scanErrors = [];
|
|
3105
|
+
const ruleFailures = [];
|
|
3106
|
+
let rulesExecuted = 0;
|
|
3107
|
+
let rulesFailed = 0;
|
|
3108
|
+
let filesScanned = 0;
|
|
3109
|
+
const results = await Promise.all(
|
|
3110
|
+
selection.candidates.map((diffFile) => this.scanFile(diffFile))
|
|
3111
|
+
);
|
|
3112
|
+
for (const result of results) {
|
|
3113
|
+
allIssues.push(...result.issues);
|
|
3114
|
+
ruleFailures.push(...result.ruleFailures);
|
|
3115
|
+
scanErrors.push(...result.scanErrors);
|
|
3116
|
+
rulesExecuted += result.rulesExecuted;
|
|
3117
|
+
rulesFailed += result.rulesFailed;
|
|
3118
|
+
if (result.scanned) {
|
|
3119
|
+
filesScanned++;
|
|
3130
3120
|
}
|
|
3131
3121
|
}
|
|
3132
|
-
const
|
|
3122
|
+
const issuesWithFingerprints = this.attachFingerprints(allIssues);
|
|
3123
|
+
const dimensions = this.groupByDimension(issuesWithFingerprints);
|
|
3133
3124
|
const overallScore = calculateOverallScore(dimensions, this.config.weights);
|
|
3134
3125
|
const grade = getGrade(overallScore);
|
|
3135
3126
|
const commitHash = await this.diffParser.getCurrentCommitHash();
|
|
3136
3127
|
return {
|
|
3128
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
3137
3129
|
version: PKG_VERSION,
|
|
3138
3130
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3139
3131
|
commit: commitHash,
|
|
3132
|
+
scanMode: selection.scanMode,
|
|
3140
3133
|
overall: {
|
|
3141
3134
|
score: overallScore,
|
|
3142
3135
|
grade,
|
|
3143
|
-
filesScanned
|
|
3144
|
-
issuesFound:
|
|
3136
|
+
filesScanned,
|
|
3137
|
+
issuesFound: issuesWithFingerprints.length
|
|
3138
|
+
},
|
|
3139
|
+
toolHealth: {
|
|
3140
|
+
rulesExecuted,
|
|
3141
|
+
rulesFailed,
|
|
3142
|
+
filesConsidered: selection.filesConsidered,
|
|
3143
|
+
filesScanned,
|
|
3144
|
+
filesExcluded: selection.filesExcluded,
|
|
3145
|
+
filesSkipped: scanErrors.length,
|
|
3146
|
+
scanErrors,
|
|
3147
|
+
ruleFailures
|
|
3145
3148
|
},
|
|
3146
3149
|
dimensions,
|
|
3147
|
-
issues:
|
|
3150
|
+
issues: issuesWithFingerprints.sort((a, b) => {
|
|
3148
3151
|
const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
|
|
3149
3152
|
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
3150
3153
|
})
|
|
3151
3154
|
};
|
|
3152
3155
|
}
|
|
3156
|
+
async scanFile(diffFile) {
|
|
3157
|
+
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
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
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
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
const filePath = resolve2(diffFile.filePath);
|
|
3190
|
+
let fileContent;
|
|
3191
|
+
try {
|
|
3192
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
3193
|
+
} catch {
|
|
3194
|
+
const content = await this.diffParser.getFileContent(diffFile.filePath);
|
|
3195
|
+
if (!content) {
|
|
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;
|
|
3212
|
+
}
|
|
3213
|
+
const addedLines = diffFile.hunks.flatMap((hunk) => {
|
|
3214
|
+
const lines = hunk.content.split("\n");
|
|
3215
|
+
const result = [];
|
|
3216
|
+
let currentLine = hunk.newStart;
|
|
3217
|
+
for (const line of lines) {
|
|
3218
|
+
if (line.startsWith("+")) {
|
|
3219
|
+
result.push({ lineNumber: currentLine, content: line.slice(1) });
|
|
3220
|
+
currentLine++;
|
|
3221
|
+
} else if (line.startsWith("-")) {
|
|
3222
|
+
} else {
|
|
3223
|
+
currentLine++;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return result;
|
|
3227
|
+
});
|
|
3228
|
+
const ruleResult = this.ruleEngine.runWithDiagnostics({
|
|
3229
|
+
filePath: diffFile.filePath,
|
|
3230
|
+
fileContent,
|
|
3231
|
+
addedLines
|
|
3232
|
+
});
|
|
3233
|
+
const issues = [...ruleResult.issues];
|
|
3234
|
+
const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
|
|
3235
|
+
maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
|
|
3236
|
+
maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
|
|
3237
|
+
maxFunctionLength: this.config.thresholds["max-function-length"],
|
|
3238
|
+
maxNestingDepth: this.config.thresholds["max-nesting-depth"],
|
|
3239
|
+
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
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
async getScanCandidates(options) {
|
|
3256
|
+
const scanMode = this.getScanMode(options);
|
|
3257
|
+
const candidates = await this.getDiffFiles(options);
|
|
3258
|
+
if (scanMode === "files") {
|
|
3259
|
+
return {
|
|
3260
|
+
scanMode,
|
|
3261
|
+
candidates,
|
|
3262
|
+
filesConsidered: options.files?.length ?? candidates.length,
|
|
3263
|
+
filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
const filteredCandidates = [];
|
|
3267
|
+
let filesExcluded = 0;
|
|
3268
|
+
for (const candidate of candidates) {
|
|
3269
|
+
if (this.shouldIncludeFile(candidate.filePath)) {
|
|
3270
|
+
filteredCandidates.push(candidate);
|
|
3271
|
+
} else {
|
|
3272
|
+
filesExcluded++;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return {
|
|
3276
|
+
scanMode,
|
|
3277
|
+
candidates: filteredCandidates,
|
|
3278
|
+
filesConsidered: candidates.length,
|
|
3279
|
+
filesExcluded
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
getScanMode(options) {
|
|
3283
|
+
if (options.staged) {
|
|
3284
|
+
return "staged";
|
|
3285
|
+
}
|
|
3286
|
+
if (options.diff) {
|
|
3287
|
+
return "diff";
|
|
3288
|
+
}
|
|
3289
|
+
if (options.files && options.files.length > 0) {
|
|
3290
|
+
return "files";
|
|
3291
|
+
}
|
|
3292
|
+
return "changed";
|
|
3293
|
+
}
|
|
3153
3294
|
async getDiffFiles(options) {
|
|
3154
3295
|
if (options.staged) {
|
|
3155
3296
|
return this.diffParser.getStagedFiles();
|
|
@@ -3158,14 +3299,10 @@ var ScanEngine = class {
|
|
|
3158
3299
|
return this.diffParser.getDiffFromRef(options.diff);
|
|
3159
3300
|
}
|
|
3160
3301
|
if (options.files && options.files.length > 0) {
|
|
3302
|
+
const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
|
|
3161
3303
|
return Promise.all(
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
try {
|
|
3165
|
-
content = await readFile(resolve2(filePath), "utf-8");
|
|
3166
|
-
} catch {
|
|
3167
|
-
}
|
|
3168
|
-
return {
|
|
3304
|
+
includedFiles.map(
|
|
3305
|
+
(filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
|
|
3169
3306
|
filePath,
|
|
3170
3307
|
status: "modified",
|
|
3171
3308
|
additions: content.split("\n").length,
|
|
@@ -3177,18 +3314,64 @@ var ScanEngine = class {
|
|
|
3177
3314
|
oldLines: 0,
|
|
3178
3315
|
newStart: 1,
|
|
3179
3316
|
newLines: content.split("\n").length,
|
|
3180
|
-
content: content.split("\n").map((
|
|
3317
|
+
content: content.split("\n").map((line) => "+" + line).join("\n")
|
|
3181
3318
|
}
|
|
3182
3319
|
]
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3320
|
+
}))
|
|
3321
|
+
)
|
|
3185
3322
|
);
|
|
3186
3323
|
}
|
|
3187
3324
|
return this.diffParser.getChangedFiles();
|
|
3188
3325
|
}
|
|
3326
|
+
shouldIncludeFile(filePath) {
|
|
3327
|
+
const normalizedPath = filePath.split(sep).join("/");
|
|
3328
|
+
const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
|
|
3329
|
+
const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3330
|
+
if (!included) {
|
|
3331
|
+
return false;
|
|
3332
|
+
}
|
|
3333
|
+
return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
|
|
3334
|
+
}
|
|
3335
|
+
matchesPattern(filePath, pattern) {
|
|
3336
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3337
|
+
regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
|
|
3338
|
+
regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
3339
|
+
regexPattern = regexPattern.replace(/\*/g, "[^/]*");
|
|
3340
|
+
regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
|
|
3341
|
+
regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
|
|
3342
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
3343
|
+
}
|
|
3189
3344
|
isTsJsFile(filePath) {
|
|
3190
3345
|
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
3191
3346
|
}
|
|
3347
|
+
attachFingerprints(issues) {
|
|
3348
|
+
const occurrenceCounts = /* @__PURE__ */ new Map();
|
|
3349
|
+
return issues.map((issue) => {
|
|
3350
|
+
const normalizedFile = this.normalizeRelativePath(issue.file);
|
|
3351
|
+
const locationComponent = `${issue.startLine}:${issue.endLine}`;
|
|
3352
|
+
const baseKey = [
|
|
3353
|
+
issue.ruleId,
|
|
3354
|
+
normalizedFile,
|
|
3355
|
+
issue.category,
|
|
3356
|
+
issue.severity,
|
|
3357
|
+
locationComponent
|
|
3358
|
+
].join("|");
|
|
3359
|
+
const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
|
|
3360
|
+
occurrenceCounts.set(baseKey, occurrenceIndex + 1);
|
|
3361
|
+
const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
|
|
3362
|
+
return {
|
|
3363
|
+
...issue,
|
|
3364
|
+
file: normalizedFile,
|
|
3365
|
+
fingerprint,
|
|
3366
|
+
fingerprintVersion: FINGERPRINT_VERSION
|
|
3367
|
+
};
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
normalizeRelativePath(filePath) {
|
|
3371
|
+
const absolutePath = resolve2(filePath);
|
|
3372
|
+
const relativePath = relative(process.cwd(), absolutePath) || filePath;
|
|
3373
|
+
return relativePath.split(sep).join("/");
|
|
3374
|
+
}
|
|
3192
3375
|
groupByDimension(issues) {
|
|
3193
3376
|
const categories = [
|
|
3194
3377
|
"security",
|
|
@@ -3199,7 +3382,7 @@ var ScanEngine = class {
|
|
|
3199
3382
|
];
|
|
3200
3383
|
const grouped = {};
|
|
3201
3384
|
for (const cat of categories) {
|
|
3202
|
-
const catIssues = issues.filter((
|
|
3385
|
+
const catIssues = issues.filter((issue) => issue.category === cat);
|
|
3203
3386
|
grouped[cat] = calculateDimensionScore(catIssues);
|
|
3204
3387
|
}
|
|
3205
3388
|
return grouped;
|
|
@@ -3219,7 +3402,11 @@ var en = {
|
|
|
3219
3402
|
issuesFound: "{{count}} issue(s) found",
|
|
3220
3403
|
issuesHeader: "Issues ({{count}}):",
|
|
3221
3404
|
noIssuesFound: "No issues found! \u{1F389}",
|
|
3222
|
-
scanned: "Scanned {{count}} file(s)"
|
|
3405
|
+
scanned: "Scanned {{count}} file(s)",
|
|
3406
|
+
healthHeader: "Tool Health",
|
|
3407
|
+
rulesFailed: "Failed rules: {{count}}",
|
|
3408
|
+
filesSkipped: "Skipped files: {{count}}",
|
|
3409
|
+
filesExcluded: "Excluded files: {{count}}"
|
|
3223
3410
|
};
|
|
3224
3411
|
var zh = {
|
|
3225
3412
|
reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
|
|
@@ -3231,7 +3418,11 @@ var zh = {
|
|
|
3231
3418
|
issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
|
|
3232
3419
|
issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
|
|
3233
3420
|
noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
|
|
3234
|
-
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
|
|
3421
|
+
scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6",
|
|
3422
|
+
healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
|
|
3423
|
+
rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
|
|
3424
|
+
filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
|
|
3425
|
+
filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
|
|
3235
3426
|
};
|
|
3236
3427
|
function renderTerminalReport(report) {
|
|
3237
3428
|
const isZh = isZhLocale();
|
|
@@ -3273,19 +3464,32 @@ function renderTerminalReport(report) {
|
|
|
3273
3464
|
};
|
|
3274
3465
|
const dims = ["security", "logic", "structure", "style", "coverage"];
|
|
3275
3466
|
for (const dim of dims) {
|
|
3276
|
-
const
|
|
3277
|
-
const dimEmoji =
|
|
3278
|
-
const color = getScoreColor(
|
|
3279
|
-
const issueCount =
|
|
3467
|
+
const dimension = report.dimensions[dim];
|
|
3468
|
+
const dimEmoji = dimension.score >= 80 ? "\u2705" : dimension.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
|
|
3469
|
+
const color = getScoreColor(dimension.score);
|
|
3470
|
+
const issueCount = dimension.issues.length;
|
|
3280
3471
|
const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
|
|
3281
3472
|
table.push([
|
|
3282
3473
|
`${dimEmoji} ${dimLabels[dim]}`,
|
|
3283
|
-
color(String(
|
|
3474
|
+
color(String(dimension.score)),
|
|
3284
3475
|
detail
|
|
3285
3476
|
]);
|
|
3286
3477
|
}
|
|
3287
3478
|
lines.push(table.toString());
|
|
3288
3479
|
lines.push("");
|
|
3480
|
+
if (report.toolHealth.rulesFailed > 0 || report.toolHealth.filesSkipped > 0 || report.toolHealth.filesExcluded > 0) {
|
|
3481
|
+
lines.push(pc.bold(t2.healthHeader));
|
|
3482
|
+
if (report.toolHealth.rulesFailed > 0) {
|
|
3483
|
+
lines.push(pc.yellow(` ${t2.rulesFailed.replace("{{count}}", String(report.toolHealth.rulesFailed))}`));
|
|
3484
|
+
}
|
|
3485
|
+
if (report.toolHealth.filesSkipped > 0) {
|
|
3486
|
+
lines.push(pc.yellow(` ${t2.filesSkipped.replace("{{count}}", String(report.toolHealth.filesSkipped))}`));
|
|
3487
|
+
}
|
|
3488
|
+
if (report.toolHealth.filesExcluded > 0) {
|
|
3489
|
+
lines.push(pc.dim(` ${t2.filesExcluded.replace("{{count}}", String(report.toolHealth.filesExcluded))}`));
|
|
3490
|
+
}
|
|
3491
|
+
lines.push("");
|
|
3492
|
+
}
|
|
3289
3493
|
if (report.issues.length > 0) {
|
|
3290
3494
|
lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
|
|
3291
3495
|
lines.push("");
|
|
@@ -3349,12 +3553,23 @@ function getScoreColor(score) {
|
|
|
3349
3553
|
|
|
3350
3554
|
// src/cli/output/json.ts
|
|
3351
3555
|
function renderJsonReport(report) {
|
|
3352
|
-
|
|
3556
|
+
const payload = {
|
|
3557
|
+
schemaVersion: report.schemaVersion,
|
|
3558
|
+
version: report.version,
|
|
3559
|
+
timestamp: report.timestamp,
|
|
3560
|
+
commit: report.commit,
|
|
3561
|
+
scanMode: report.scanMode,
|
|
3562
|
+
overall: report.overall,
|
|
3563
|
+
toolHealth: report.toolHealth,
|
|
3564
|
+
dimensions: report.dimensions,
|
|
3565
|
+
issues: report.issues
|
|
3566
|
+
};
|
|
3567
|
+
return JSON.stringify(payload, null, 2);
|
|
3353
3568
|
}
|
|
3354
3569
|
|
|
3355
3570
|
// src/cli/commands/scan.ts
|
|
3356
3571
|
function createScanCommand() {
|
|
3357
|
-
const cmd = new Command("scan").description("
|
|
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) => {
|
|
3358
3573
|
try {
|
|
3359
3574
|
const config = await loadConfig();
|
|
3360
3575
|
const engine = new ScanEngine(config);
|
|
@@ -3389,7 +3604,7 @@ function createScanCommand() {
|
|
|
3389
3604
|
// src/cli/commands/report.ts
|
|
3390
3605
|
import { Command as Command2 } from "commander";
|
|
3391
3606
|
function createReportCommand() {
|
|
3392
|
-
const cmd = new Command2("report").description("
|
|
3607
|
+
const cmd = new Command2("report").description("Render a report for a diff-based scan (transitional wrapper around scan)").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref for report presentation", "HEAD~1").action(async (opts) => {
|
|
3393
3608
|
try {
|
|
3394
3609
|
const config = await loadConfig();
|
|
3395
3610
|
const engine = new ScanEngine(config);
|
|
@@ -3413,14 +3628,14 @@ function createReportCommand() {
|
|
|
3413
3628
|
|
|
3414
3629
|
// src/cli/commands/init.ts
|
|
3415
3630
|
import { writeFile } from "fs/promises";
|
|
3416
|
-
import { existsSync as
|
|
3631
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3417
3632
|
import { resolve as resolve3 } from "path";
|
|
3418
3633
|
import { Command as Command3 } from "commander";
|
|
3419
3634
|
import pc2 from "picocolors";
|
|
3420
3635
|
function createInitCommand() {
|
|
3421
3636
|
const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
|
|
3422
3637
|
const configPath = resolve3(".codetrust.yml");
|
|
3423
|
-
if (
|
|
3638
|
+
if (existsSync3(configPath)) {
|
|
3424
3639
|
console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
|
|
3425
3640
|
return;
|
|
3426
3641
|
}
|
|
@@ -3494,7 +3709,7 @@ function formatSeverity2(severity) {
|
|
|
3494
3709
|
|
|
3495
3710
|
// src/cli/commands/hook.ts
|
|
3496
3711
|
import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
|
|
3497
|
-
import { existsSync as
|
|
3712
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3498
3713
|
import { resolve as resolve4, join as join2 } from "path";
|
|
3499
3714
|
import { Command as Command5 } from "commander";
|
|
3500
3715
|
import pc4 from "picocolors";
|
|
@@ -3511,17 +3726,17 @@ function createHookCommand() {
|
|
|
3511
3726
|
const cmd = new Command5("hook").description("Manage git hooks");
|
|
3512
3727
|
cmd.command("install").description("Install pre-commit hook").action(async () => {
|
|
3513
3728
|
const gitDir = resolve4(".git");
|
|
3514
|
-
if (!
|
|
3729
|
+
if (!existsSync4(gitDir)) {
|
|
3515
3730
|
console.error(pc4.red("Error: Not a git repository."));
|
|
3516
3731
|
process.exit(1);
|
|
3517
3732
|
}
|
|
3518
3733
|
const hooksDir = join2(gitDir, "hooks");
|
|
3519
3734
|
const hookPath = join2(hooksDir, "pre-commit");
|
|
3520
3735
|
try {
|
|
3521
|
-
if (!
|
|
3736
|
+
if (!existsSync4(hooksDir)) {
|
|
3522
3737
|
await mkdir(hooksDir, { recursive: true });
|
|
3523
3738
|
}
|
|
3524
|
-
if (
|
|
3739
|
+
if (existsSync4(hookPath)) {
|
|
3525
3740
|
console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
|
|
3526
3741
|
console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
|
|
3527
3742
|
return;
|