dep-brain 1.3.0 → 1.4.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/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/depbrain.output.schema.json +22 -3
- package/dist/checks/risk.js +179 -25
- package/dist/core/analyzer.d.ts +12 -1
- package/dist/core/analyzer.js +47 -7
- package/dist/core/graph-builder.d.ts +1 -0
- package/dist/core/graph-builder.js +153 -58
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +9 -2
- package/dist/reporters/dashboard.js +44 -5
- package/dist/reporters/markdown.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.4.0
|
|
6
|
+
|
|
7
|
+
- Added lockfile dependency-edge parsing so transitive relationships are available from npm, pnpm, and yarn lockfiles.
|
|
8
|
+
- Changed risk analysis from direct-package-only output to direct-owner risk summaries with `transitiveRiskScore` and `riskyTransitiveDeps`.
|
|
9
|
+
- Added transitive dependency counts and risky transitive counts to `riskFactors`.
|
|
10
|
+
- Updated console, markdown, and dashboard reports to highlight transitive risk hotspots.
|
|
11
|
+
- Bumped analysis output contract to `1.5` and added regression coverage for transitive risk propagation.
|
|
12
|
+
|
|
5
13
|
## 1.3.0
|
|
6
14
|
|
|
7
15
|
- Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
- Detect likely unused dependencies from source imports and scripts
|
|
22
22
|
- Detect outdated packages
|
|
23
23
|
- Highlight dependency risk signals
|
|
24
|
+
- Show which direct dependency introduces risky transitive packages
|
|
24
25
|
- Score package trust using supply-chain metadata
|
|
25
26
|
- Generate a simple project health score
|
|
26
27
|
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
@@ -39,6 +40,7 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
39
40
|
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
40
41
|
- Outdated dependency reporting with `major`, `minor`, and `patch` classification
|
|
41
42
|
- Risk analysis based on npm package metadata
|
|
43
|
+
- Transitive risk ownership and path tracing for direct dependencies
|
|
42
44
|
- Confidence scores, reason codes, explanations, and recommendations for findings
|
|
43
45
|
- Config loading from `depbrain.config.json`
|
|
44
46
|
- Ignore rules for noisy dependencies and checks
|
|
@@ -379,7 +381,7 @@ src/
|
|
|
379
381
|
|
|
380
382
|
The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
381
383
|
|
|
382
|
-
Risk findings now include a `trustScore
|
|
384
|
+
Risk findings now include a `trustScore`, structured `riskFactors`, `transitiveRiskScore`, and `riskyTransitiveDeps` path traces so teams can see which direct package introduces supply-chain risk.
|
|
383
385
|
|
|
384
386
|
## Repository Notes
|
|
385
387
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"riskFactors": {
|
|
21
21
|
"type": "object",
|
|
22
|
-
"required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType"],
|
|
22
|
+
"required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType", "transitiveDependencyCount", "riskyTransitiveCount"],
|
|
23
23
|
"additionalProperties": false,
|
|
24
24
|
"properties": {
|
|
25
25
|
"daysSincePublish": { "type": ["number", "null"] },
|
|
@@ -28,7 +28,21 @@
|
|
|
28
28
|
"versionCount": { "type": ["number", "null"] },
|
|
29
29
|
"recentReleaseCount": { "type": ["number", "null"] },
|
|
30
30
|
"hasRepository": { "type": "boolean" },
|
|
31
|
-
"dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
|
|
31
|
+
"dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] },
|
|
32
|
+
"transitiveDependencyCount": { "type": "number" },
|
|
33
|
+
"riskyTransitiveCount": { "type": "number" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"riskTransitiveDependency": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["name", "trustScore", "confidence", "reasons", "introducedByPaths"],
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"properties": {
|
|
41
|
+
"name": { "type": "string" },
|
|
42
|
+
"trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
|
|
43
|
+
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
44
|
+
"reasons": { "type": "array", "items": { "type": "string" } },
|
|
45
|
+
"introducedByPaths": { "type": "array", "items": { "type": "string" } }
|
|
32
46
|
}
|
|
33
47
|
},
|
|
34
48
|
"ownershipSummary": {
|
|
@@ -159,7 +173,7 @@
|
|
|
159
173
|
"type": "array",
|
|
160
174
|
"items": {
|
|
161
175
|
"type": "object",
|
|
162
|
-
"required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
|
|
176
|
+
"required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "transitiveRiskScore", "riskyTransitiveDeps", "recommendation"],
|
|
163
177
|
"additionalProperties": false,
|
|
164
178
|
"properties": {
|
|
165
179
|
"name": { "type": "string" },
|
|
@@ -170,6 +184,11 @@
|
|
|
170
184
|
"explanation": { "type": "array", "items": { "type": "string" } },
|
|
171
185
|
"trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
|
|
172
186
|
"riskFactors": { "$ref": "#/properties/riskFactors" },
|
|
187
|
+
"transitiveRiskScore": { "type": "number" },
|
|
188
|
+
"riskyTransitiveDeps": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": { "$ref": "#/properties/riskTransitiveDependency" }
|
|
191
|
+
},
|
|
173
192
|
"recommendation": { "$ref": "#/properties/recommendation" }
|
|
174
193
|
}
|
|
175
194
|
}
|
package/dist/checks/risk.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
2
|
export async function findRiskDependencies(graph, options = {}) {
|
|
3
3
|
const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
|
|
4
|
-
const
|
|
4
|
+
const thresholds = options.thresholds;
|
|
5
|
+
const allNames = Object.keys({
|
|
5
6
|
...graph.dependencies,
|
|
6
|
-
...graph.devDependencies
|
|
7
|
+
...graph.devDependencies,
|
|
8
|
+
...(graph.lockPackages ?? {})
|
|
7
9
|
});
|
|
8
|
-
const
|
|
10
|
+
const assessments = new Map();
|
|
11
|
+
const results = await mapWithConcurrency(allNames, 8, async (name) => {
|
|
9
12
|
const metadata = await resolvePackageMetadata(name);
|
|
10
13
|
if (!metadata) {
|
|
11
14
|
return null;
|
|
@@ -15,24 +18,25 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
15
18
|
: graph.devDependencies[name]
|
|
16
19
|
? "devDependencies"
|
|
17
20
|
: "unknown";
|
|
18
|
-
const assessment = assessRisk(metadata, dependencyType,
|
|
19
|
-
if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
21
|
+
const assessment = assessRisk(metadata, dependencyType, thresholds, 0);
|
|
22
22
|
return {
|
|
23
23
|
name,
|
|
24
|
-
|
|
25
|
-
confidence: assessment.confidence,
|
|
26
|
-
reasonCodes: assessment.reasonCodes,
|
|
27
|
-
explanation: assessment.reasons,
|
|
28
|
-
trustScore: assessment.trustScore,
|
|
29
|
-
riskFactors: assessment.riskFactors,
|
|
30
|
-
recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
|
|
24
|
+
assessment
|
|
31
25
|
};
|
|
32
26
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
if (result) {
|
|
29
|
+
assessments.set(result.name, result.assessment);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const directNames = Object.keys({
|
|
33
|
+
...graph.dependencies,
|
|
34
|
+
...graph.devDependencies
|
|
35
|
+
}).sort((left, right) => left.localeCompare(right));
|
|
36
|
+
const risks = directNames
|
|
37
|
+
.map((name) => buildDirectRiskEntry(name, graph, assessments, thresholds))
|
|
38
|
+
.filter((item) => item !== null);
|
|
39
|
+
return risks.sort((left, right) => left.name.localeCompare(right.name));
|
|
36
40
|
}
|
|
37
41
|
async function mapWithConcurrency(items, limit, mapper) {
|
|
38
42
|
const results = new Array(items.length);
|
|
@@ -68,12 +72,120 @@ export async function runRiskCheck(graph, options = {}) {
|
|
|
68
72
|
name: item.name,
|
|
69
73
|
reasons: item.reasons,
|
|
70
74
|
trustScore: item.trustScore,
|
|
71
|
-
riskFactors: item.riskFactors
|
|
75
|
+
riskFactors: item.riskFactors,
|
|
76
|
+
transitiveRiskScore: item.transitiveRiskScore,
|
|
77
|
+
riskyTransitiveDeps: item.riskyTransitiveDeps
|
|
72
78
|
}
|
|
73
79
|
}))
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
function
|
|
82
|
+
function buildDirectRiskEntry(name, graph, assessments, thresholds) {
|
|
83
|
+
const dependencyType = graph.dependencies[name]
|
|
84
|
+
? "dependencies"
|
|
85
|
+
: graph.devDependencies[name]
|
|
86
|
+
? "devDependencies"
|
|
87
|
+
: "unknown";
|
|
88
|
+
const selfAssessment = assessments.get(name) ?? buildUnknownAssessment(name, dependencyType);
|
|
89
|
+
const transitive = collectTransitiveRisks(name, graph, assessments);
|
|
90
|
+
const reasonCodes = [...selfAssessment.reasonCodes];
|
|
91
|
+
const reasons = [...selfAssessment.reasons];
|
|
92
|
+
const explanation = [...selfAssessment.reasons];
|
|
93
|
+
if (transitive.riskyTransitiveDeps.length > 0) {
|
|
94
|
+
reasons.push(`Introduces ${transitive.riskyTransitiveDeps.length} risky transitive dependenc${transitive.riskyTransitiveDeps.length === 1 ? "y" : "ies"}`);
|
|
95
|
+
reasonCodes.push("risky_transitive_dependencies");
|
|
96
|
+
explanation.push(`Transitive paths: ${transitive.riskyTransitiveDeps
|
|
97
|
+
.flatMap((item) => item.introducedByPaths)
|
|
98
|
+
.slice(0, 3)
|
|
99
|
+
.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
if (transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50)) {
|
|
102
|
+
reasons.push("Large transitive dependency tree");
|
|
103
|
+
reasonCodes.push("dependency_bloat");
|
|
104
|
+
explanation.push(`${name} introduces ${transitive.transitiveDependencyCount} transitive dependencies.`);
|
|
105
|
+
}
|
|
106
|
+
const transitiveRiskScore = transitive.riskyTransitiveDeps.reduce((total, item) => total + trustScoreWeight(item.trustScore), 0);
|
|
107
|
+
const combinedConfidence = Math.min(0.99, Math.max(selfAssessment.confidence, transitive.riskyTransitiveDeps.reduce((maxConfidence, item) => Math.max(maxConfidence, item.confidence), 0.5)));
|
|
108
|
+
const trustScore = combineTrustScores(selfAssessment.trustScore, transitive.highestTrustScore);
|
|
109
|
+
const shouldReport = shouldReportRisk(selfAssessment.trustScore, dependencyType) ||
|
|
110
|
+
transitive.riskyTransitiveDeps.length > 0 ||
|
|
111
|
+
transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50);
|
|
112
|
+
if (!shouldReport || reasons.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
reasons,
|
|
118
|
+
confidence: combinedConfidence,
|
|
119
|
+
reasonCodes: dedupeStrings(reasonCodes),
|
|
120
|
+
explanation: dedupeStrings(explanation),
|
|
121
|
+
trustScore,
|
|
122
|
+
riskFactors: {
|
|
123
|
+
...selfAssessment.riskFactors,
|
|
124
|
+
dependencyType,
|
|
125
|
+
transitiveDependencyCount: transitive.transitiveDependencyCount,
|
|
126
|
+
riskyTransitiveCount: transitive.riskyTransitiveDeps.length
|
|
127
|
+
},
|
|
128
|
+
transitiveRiskScore,
|
|
129
|
+
riskyTransitiveDeps: transitive.riskyTransitiveDeps,
|
|
130
|
+
recommendation: buildRiskRecommendation(reasons, combinedConfidence, trustScore, transitive.riskyTransitiveDeps.length)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function collectTransitiveRisks(directName, graph, assessments) {
|
|
134
|
+
const visited = new Set();
|
|
135
|
+
const queue = (graph.lockDependencies?.[directName] ?? []).map((name) => ({
|
|
136
|
+
name,
|
|
137
|
+
path: [directName, name]
|
|
138
|
+
}));
|
|
139
|
+
const riskyByName = new Map();
|
|
140
|
+
let highestTrustScore = "high";
|
|
141
|
+
while (queue.length > 0) {
|
|
142
|
+
const current = queue.shift();
|
|
143
|
+
if (!current || visited.has(current.name)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
visited.add(current.name);
|
|
147
|
+
const assessment = assessments.get(current.name);
|
|
148
|
+
if (assessment && shouldReportRisk(assessment.trustScore, assessment.riskFactors.dependencyType)) {
|
|
149
|
+
highestTrustScore = combineTrustScores(highestTrustScore, assessment.trustScore);
|
|
150
|
+
const existing = riskyByName.get(current.name);
|
|
151
|
+
const pathTrace = current.path.join(" -> ");
|
|
152
|
+
if (existing) {
|
|
153
|
+
existing.introducedByPaths.push(pathTrace);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
riskyByName.set(current.name, {
|
|
157
|
+
name: current.name,
|
|
158
|
+
trustScore: assessment.trustScore,
|
|
159
|
+
confidence: assessment.confidence,
|
|
160
|
+
reasons: assessment.reasons,
|
|
161
|
+
introducedByPaths: [pathTrace]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const nextDependencies = graph.lockDependencies?.[current.name] ?? [];
|
|
166
|
+
for (const dependency of nextDependencies) {
|
|
167
|
+
if (!visited.has(dependency)) {
|
|
168
|
+
queue.push({
|
|
169
|
+
name: dependency,
|
|
170
|
+
path: [...current.path, dependency]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
transitiveDependencyCount: visited.size,
|
|
177
|
+
riskyTransitiveDeps: Array.from(riskyByName.values())
|
|
178
|
+
.map((item) => ({
|
|
179
|
+
...item,
|
|
180
|
+
introducedByPaths: dedupeStrings(item.introducedByPaths).slice(0, 3)
|
|
181
|
+
}))
|
|
182
|
+
.sort((left, right) => trustScoreWeight(right.trustScore) - trustScoreWeight(left.trustScore) ||
|
|
183
|
+
right.confidence - left.confidence ||
|
|
184
|
+
left.name.localeCompare(right.name)),
|
|
185
|
+
highestTrustScore
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function assessRisk(metadata, dependencyType, thresholds, transitiveDependencyCount) {
|
|
77
189
|
const reasons = [];
|
|
78
190
|
const reasonCodes = [];
|
|
79
191
|
let weight = 0;
|
|
@@ -127,6 +239,7 @@ function assessRisk(metadata, dependencyType, thresholds) {
|
|
|
127
239
|
? "medium"
|
|
128
240
|
: "high";
|
|
129
241
|
return {
|
|
242
|
+
name: "",
|
|
130
243
|
confidence,
|
|
131
244
|
trustScore,
|
|
132
245
|
reasons,
|
|
@@ -138,7 +251,29 @@ function assessRisk(metadata, dependencyType, thresholds) {
|
|
|
138
251
|
versionCount: metadata.versionCount,
|
|
139
252
|
recentReleaseCount: metadata.recentReleaseCount,
|
|
140
253
|
hasRepository: Boolean(metadata.repository),
|
|
141
|
-
dependencyType
|
|
254
|
+
dependencyType,
|
|
255
|
+
transitiveDependencyCount,
|
|
256
|
+
riskyTransitiveCount: 0
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function buildUnknownAssessment(name, dependencyType) {
|
|
261
|
+
return {
|
|
262
|
+
name,
|
|
263
|
+
confidence: 0.5,
|
|
264
|
+
trustScore: "high",
|
|
265
|
+
reasons: [],
|
|
266
|
+
reasonCodes: [],
|
|
267
|
+
riskFactors: {
|
|
268
|
+
daysSincePublish: null,
|
|
269
|
+
downloads: null,
|
|
270
|
+
maintainersCount: null,
|
|
271
|
+
versionCount: null,
|
|
272
|
+
recentReleaseCount: null,
|
|
273
|
+
hasRepository: false,
|
|
274
|
+
dependencyType,
|
|
275
|
+
transitiveDependencyCount: 0,
|
|
276
|
+
riskyTransitiveCount: 0
|
|
142
277
|
}
|
|
143
278
|
};
|
|
144
279
|
}
|
|
@@ -160,14 +295,33 @@ function shouldReportRisk(trustScore, dependencyType) {
|
|
|
160
295
|
}
|
|
161
296
|
return true;
|
|
162
297
|
}
|
|
163
|
-
function buildRiskRecommendation(reasons, confidence, trustScore) {
|
|
298
|
+
function buildRiskRecommendation(reasons, confidence, trustScore, riskyTransitiveCount) {
|
|
164
299
|
return {
|
|
165
300
|
action: "review",
|
|
166
|
-
priority: trustScore === "low" || confidence >= 0.8
|
|
301
|
+
priority: trustScore === "low" || confidence >= 0.8 || riskyTransitiveCount >= 2
|
|
302
|
+
? "high"
|
|
303
|
+
: "medium",
|
|
167
304
|
safety: "caution",
|
|
168
|
-
summary:
|
|
169
|
-
?
|
|
170
|
-
:
|
|
305
|
+
summary: riskyTransitiveCount > 0
|
|
306
|
+
? `Review this direct dependency and its transitive chain before upgrading or keeping it.`
|
|
307
|
+
: trustScore === "low"
|
|
308
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
309
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
171
310
|
reasons
|
|
172
311
|
};
|
|
173
312
|
}
|
|
313
|
+
function trustScoreWeight(value) {
|
|
314
|
+
if (value === "low") {
|
|
315
|
+
return 3;
|
|
316
|
+
}
|
|
317
|
+
if (value === "medium") {
|
|
318
|
+
return 2;
|
|
319
|
+
}
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
function combineTrustScores(left, right) {
|
|
323
|
+
return trustScoreWeight(left) >= trustScoreWeight(right) ? left : right;
|
|
324
|
+
}
|
|
325
|
+
function dedupeStrings(values) {
|
|
326
|
+
return Array.from(new Set(values));
|
|
327
|
+
}
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -43,8 +43,17 @@ export interface RiskFactors {
|
|
|
43
43
|
recentReleaseCount: number | null;
|
|
44
44
|
hasRepository: boolean;
|
|
45
45
|
dependencyType: "dependencies" | "devDependencies" | "unknown";
|
|
46
|
+
transitiveDependencyCount: number;
|
|
47
|
+
riskyTransitiveCount: number;
|
|
46
48
|
}
|
|
47
49
|
export type TrustScore = "high" | "medium" | "low";
|
|
50
|
+
export interface RiskTransitiveDependency {
|
|
51
|
+
name: string;
|
|
52
|
+
trustScore: TrustScore;
|
|
53
|
+
confidence: number;
|
|
54
|
+
reasons: string[];
|
|
55
|
+
introducedByPaths: string[];
|
|
56
|
+
}
|
|
48
57
|
export interface DuplicateDependency {
|
|
49
58
|
name: string;
|
|
50
59
|
versions: string[];
|
|
@@ -85,6 +94,8 @@ export interface RiskDependency {
|
|
|
85
94
|
explanation: string[];
|
|
86
95
|
trustScore: TrustScore;
|
|
87
96
|
riskFactors: RiskFactors;
|
|
97
|
+
transitiveRiskScore: number;
|
|
98
|
+
riskyTransitiveDeps: RiskTransitiveDependency[];
|
|
88
99
|
recommendation: Recommendation;
|
|
89
100
|
}
|
|
90
101
|
export interface TopIssue {
|
|
@@ -133,7 +144,7 @@ export interface PackageAnalysisResult {
|
|
|
133
144
|
topIssues: TopIssue[];
|
|
134
145
|
extensions: Record<string, unknown>;
|
|
135
146
|
}
|
|
136
|
-
export declare const OUTPUT_VERSION = "1.
|
|
147
|
+
export declare const OUTPUT_VERSION = "1.5";
|
|
137
148
|
export interface ScoreBreakdown {
|
|
138
149
|
baseScore: number;
|
|
139
150
|
duplicates: number;
|
package/dist/core/analyzer.js
CHANGED
|
@@ -9,7 +9,7 @@ import { buildDependencyGraph } from "./graph-builder.js";
|
|
|
9
9
|
import { PluginManager } from "./plugin-manager.js";
|
|
10
10
|
import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
|
|
11
11
|
import { buildAnalysisContext } from "./context.js";
|
|
12
|
-
export const OUTPUT_VERSION = "1.
|
|
12
|
+
export const OUTPUT_VERSION = "1.5";
|
|
13
13
|
export async function analyzeProject(options = {}) {
|
|
14
14
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
15
15
|
const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
|
|
@@ -396,6 +396,10 @@ function mapRiskIssues(issues) {
|
|
|
396
396
|
explanation: normalizeStringArray(issue.explanation),
|
|
397
397
|
trustScore: normalizeTrustScore(issue.meta?.trustScore),
|
|
398
398
|
riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
|
|
399
|
+
transitiveRiskScore: typeof issue.meta?.transitiveRiskScore === "number"
|
|
400
|
+
? issue.meta.transitiveRiskScore
|
|
401
|
+
: 0,
|
|
402
|
+
riskyTransitiveDeps: normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps),
|
|
399
403
|
recommendation: buildRiskRecommendation(issue)
|
|
400
404
|
}));
|
|
401
405
|
}
|
|
@@ -459,13 +463,18 @@ function buildRiskRecommendation(issue) {
|
|
|
459
463
|
const reasons = normalizeStringArray(issue.explanation);
|
|
460
464
|
const confidence = normalizeConfidence(issue.confidence);
|
|
461
465
|
const trustScore = normalizeTrustScore(issue.meta?.trustScore);
|
|
466
|
+
const riskyTransitiveDeps = normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps);
|
|
462
467
|
return {
|
|
463
468
|
action: "review",
|
|
464
|
-
priority: trustScore === "low" || confidence >= 0.79
|
|
469
|
+
priority: trustScore === "low" || confidence >= 0.79 || riskyTransitiveDeps.length >= 2
|
|
470
|
+
? "high"
|
|
471
|
+
: "medium",
|
|
465
472
|
safety: "caution",
|
|
466
|
-
summary:
|
|
467
|
-
? "
|
|
468
|
-
:
|
|
473
|
+
summary: riskyTransitiveDeps.length > 0
|
|
474
|
+
? "Review this direct dependency and its transitive chain before upgrading or keeping it."
|
|
475
|
+
: trustScore === "low"
|
|
476
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
477
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
469
478
|
reasons
|
|
470
479
|
};
|
|
471
480
|
}
|
|
@@ -604,7 +613,9 @@ function normalizeRiskFactors(value) {
|
|
|
604
613
|
versionCount: null,
|
|
605
614
|
recentReleaseCount: null,
|
|
606
615
|
hasRepository: false,
|
|
607
|
-
dependencyType: "unknown"
|
|
616
|
+
dependencyType: "unknown",
|
|
617
|
+
transitiveDependencyCount: 0,
|
|
618
|
+
riskyTransitiveCount: 0
|
|
608
619
|
};
|
|
609
620
|
}
|
|
610
621
|
const factors = value;
|
|
@@ -617,9 +628,38 @@ function normalizeRiskFactors(value) {
|
|
|
617
628
|
hasRepository: factors.hasRepository === true,
|
|
618
629
|
dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
|
|
619
630
|
? factors.dependencyType
|
|
620
|
-
: "unknown"
|
|
631
|
+
: "unknown",
|
|
632
|
+
transitiveDependencyCount: typeof factors.transitiveDependencyCount === "number" ? factors.transitiveDependencyCount : 0,
|
|
633
|
+
riskyTransitiveCount: typeof factors.riskyTransitiveCount === "number" ? factors.riskyTransitiveCount : 0
|
|
621
634
|
};
|
|
622
635
|
}
|
|
636
|
+
function normalizeRiskTransitiveDependencies(value) {
|
|
637
|
+
if (!Array.isArray(value)) {
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
return value
|
|
641
|
+
.map((entry) => {
|
|
642
|
+
if (!entry || typeof entry !== "object") {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const item = entry;
|
|
646
|
+
if (typeof item.name !== "string" ||
|
|
647
|
+
(item.trustScore !== "high" && item.trustScore !== "medium" && item.trustScore !== "low") ||
|
|
648
|
+
typeof item.confidence !== "number" ||
|
|
649
|
+
!Array.isArray(item.reasons) ||
|
|
650
|
+
!Array.isArray(item.introducedByPaths)) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
name: item.name,
|
|
655
|
+
trustScore: item.trustScore,
|
|
656
|
+
confidence: normalizeConfidence(item.confidence),
|
|
657
|
+
reasons: item.reasons.filter((reason) => typeof reason === "string"),
|
|
658
|
+
introducedByPaths: item.introducedByPaths.filter((trace) => typeof trace === "string")
|
|
659
|
+
};
|
|
660
|
+
})
|
|
661
|
+
.filter((entry) => entry !== null);
|
|
662
|
+
}
|
|
623
663
|
function normalizeWorkspaceUsage(value) {
|
|
624
664
|
if (!Array.isArray(value)) {
|
|
625
665
|
return [];
|
|
@@ -11,5 +11,6 @@ export interface DependencyGraph {
|
|
|
11
11
|
overrides: Record<string, unknown>;
|
|
12
12
|
scripts: Record<string, string>;
|
|
13
13
|
lockPackages: Record<string, LockPackageInstance[]>;
|
|
14
|
+
lockDependencies: Record<string, string[]>;
|
|
14
15
|
}
|
|
15
16
|
export declare function buildDependencyGraph(rootDir: string): Promise<DependencyGraph>;
|
|
@@ -7,32 +7,29 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
7
7
|
const pnpmLockfilePath = path.join(rootDir, "pnpm-lock.yaml");
|
|
8
8
|
const yarnLockfilePath = path.join(rootDir, "yarn.lock");
|
|
9
9
|
const packageJson = await readJsonFile(packageJsonPath);
|
|
10
|
-
const lockPackages = new Map();
|
|
11
10
|
try {
|
|
12
11
|
const packageLock = await readJsonFile(lockfilePath);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const instances = lockPackages.get(name) ?? new Map();
|
|
29
|
-
const normalizedPath = `node_modules/${name}`;
|
|
30
|
-
instances.set(normalizedPath, { path: normalizedPath, version: details.version });
|
|
31
|
-
lockPackages.set(name, instances);
|
|
32
|
-
}
|
|
12
|
+
const parsed = parseNpmLockfile(packageLock, {
|
|
13
|
+
...packageJson.dependencies,
|
|
14
|
+
...packageJson.devDependencies
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
rootDir,
|
|
18
|
+
packageJsonPath,
|
|
19
|
+
lockfilePath,
|
|
20
|
+
dependencies: packageJson.dependencies ?? {},
|
|
21
|
+
devDependencies: packageJson.devDependencies ?? {},
|
|
22
|
+
overrides: packageJson.overrides ?? {},
|
|
23
|
+
scripts: packageJson.scripts ?? {},
|
|
24
|
+
lockPackages: parsed.lockPackages,
|
|
25
|
+
lockDependencies: parsed.lockDependencies
|
|
26
|
+
};
|
|
33
27
|
}
|
|
34
28
|
catch {
|
|
35
|
-
const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath
|
|
29
|
+
const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, {
|
|
30
|
+
...packageJson.dependencies,
|
|
31
|
+
...packageJson.devDependencies
|
|
32
|
+
});
|
|
36
33
|
return {
|
|
37
34
|
rootDir,
|
|
38
35
|
packageJsonPath,
|
|
@@ -41,29 +38,19 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
41
38
|
devDependencies: packageJson.devDependencies ?? {},
|
|
42
39
|
overrides: packageJson.overrides ?? {},
|
|
43
40
|
scripts: packageJson.scripts ?? {},
|
|
44
|
-
lockPackages: fallbackLockfile.lockPackages
|
|
41
|
+
lockPackages: fallbackLockfile.lockPackages,
|
|
42
|
+
lockDependencies: fallbackLockfile.lockDependencies
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
|
-
return {
|
|
48
|
-
rootDir,
|
|
49
|
-
packageJsonPath,
|
|
50
|
-
lockfilePath,
|
|
51
|
-
dependencies: packageJson.dependencies ?? {},
|
|
52
|
-
devDependencies: packageJson.devDependencies ?? {},
|
|
53
|
-
overrides: packageJson.overrides ?? {},
|
|
54
|
-
scripts: packageJson.scripts ?? {},
|
|
55
|
-
lockPackages: Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
56
|
-
name,
|
|
57
|
-
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
58
|
-
]))
|
|
59
|
-
};
|
|
60
45
|
}
|
|
61
|
-
async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
|
|
46
|
+
async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, rootDependencies) {
|
|
62
47
|
try {
|
|
63
48
|
const content = await fs.readFile(pnpmLockfilePath, "utf8");
|
|
49
|
+
const parsed = parsePnpmLockfile(content, rootDependencies);
|
|
64
50
|
return {
|
|
65
51
|
lockfilePath: pnpmLockfilePath,
|
|
66
|
-
lockPackages:
|
|
52
|
+
lockPackages: parsed.lockPackages,
|
|
53
|
+
lockDependencies: parsed.lockDependencies
|
|
67
54
|
};
|
|
68
55
|
}
|
|
69
56
|
catch {
|
|
@@ -71,58 +58,150 @@ async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
|
|
|
71
58
|
}
|
|
72
59
|
try {
|
|
73
60
|
const content = await fs.readFile(yarnLockfilePath, "utf8");
|
|
61
|
+
const parsed = parseYarnLockfile(content, rootDependencies);
|
|
74
62
|
return {
|
|
75
63
|
lockfilePath: yarnLockfilePath,
|
|
76
|
-
lockPackages:
|
|
64
|
+
lockPackages: parsed.lockPackages,
|
|
65
|
+
lockDependencies: parsed.lockDependencies
|
|
77
66
|
};
|
|
78
67
|
}
|
|
79
68
|
catch {
|
|
80
69
|
return {
|
|
81
|
-
lockPackages: {}
|
|
70
|
+
lockPackages: {},
|
|
71
|
+
lockDependencies: {}
|
|
82
72
|
};
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
75
|
+
function parseNpmLockfile(packageLock, rootDependencies) {
|
|
76
|
+
const lockPackages = new Map();
|
|
77
|
+
const lockDependencies = new Map();
|
|
78
|
+
for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
|
|
79
|
+
const name = details.name ?? extractPackageName(packagePath);
|
|
80
|
+
const version = details.version;
|
|
81
|
+
if (name && version) {
|
|
82
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
83
|
+
const normalizedPath = packagePath || "node_modules/" + name;
|
|
84
|
+
instances.set(normalizedPath, { path: normalizedPath, version });
|
|
85
|
+
lockPackages.set(name, instances);
|
|
86
|
+
}
|
|
87
|
+
if (name) {
|
|
88
|
+
addDependencyNames(lockDependencies, name, Object.keys(details.dependencies ?? {}));
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
|
|
92
|
+
if (details.version) {
|
|
93
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
94
|
+
const normalizedPath = `node_modules/${name}`;
|
|
95
|
+
instances.set(normalizedPath, { path: normalizedPath, version: details.version });
|
|
96
|
+
lockPackages.set(name, instances);
|
|
97
|
+
}
|
|
98
|
+
addDependencyNames(lockDependencies, name, Object.keys(details.requires ?? {}));
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
101
|
+
return {
|
|
102
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
103
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
104
|
+
};
|
|
94
105
|
}
|
|
95
|
-
function parsePnpmLockfile(content) {
|
|
106
|
+
function parsePnpmLockfile(content, rootDependencies) {
|
|
96
107
|
const lockPackages = new Map();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
const lockDependencies = new Map();
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
let currentName = null;
|
|
111
|
+
let currentVersion = null;
|
|
112
|
+
let inDependenciesBlock = false;
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const packageMatch = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
|
|
115
|
+
if (packageMatch) {
|
|
116
|
+
currentName = packageMatch[1];
|
|
117
|
+
currentVersion = packageMatch[2];
|
|
118
|
+
inDependenciesBlock = false;
|
|
119
|
+
addLockPackage(lockPackages, currentName, `pnpm:${currentName}@${currentVersion}`, currentVersion);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!currentName) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
|
|
126
|
+
inDependenciesBlock = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/^\s{4}\S/.test(line) && !/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
|
|
130
|
+
inDependenciesBlock = false;
|
|
131
|
+
}
|
|
132
|
+
if (!inDependenciesBlock) {
|
|
100
133
|
continue;
|
|
101
134
|
}
|
|
102
|
-
|
|
135
|
+
const dependencyMatch = line.match(/^\s{6}((?:@[^/]+\/)?[^:\s]+):\s*(.+)?$/);
|
|
136
|
+
if (!dependencyMatch) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
addDependencyNames(lockDependencies, currentName, [dependencyMatch[1]]);
|
|
103
140
|
}
|
|
104
|
-
|
|
141
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
142
|
+
return {
|
|
143
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
144
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
145
|
+
};
|
|
105
146
|
}
|
|
106
|
-
function parseYarnLockfile(content) {
|
|
147
|
+
function parseYarnLockfile(content, rootDependencies) {
|
|
107
148
|
const lockPackages = new Map();
|
|
149
|
+
const lockDependencies = new Map();
|
|
150
|
+
const lines = content.split(/\r?\n/);
|
|
108
151
|
let currentNames = [];
|
|
109
|
-
|
|
152
|
+
let currentVersion = null;
|
|
153
|
+
let inDependenciesBlock = false;
|
|
154
|
+
for (const line of lines) {
|
|
110
155
|
if (line.trim().length === 0 || line.startsWith("#")) {
|
|
111
156
|
continue;
|
|
112
157
|
}
|
|
113
158
|
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
114
159
|
currentNames = extractYarnEntryNames(line.slice(0, -1));
|
|
160
|
+
currentVersion = null;
|
|
161
|
+
inDependenciesBlock = false;
|
|
115
162
|
continue;
|
|
116
163
|
}
|
|
117
164
|
const versionMatch = line.match(/^\s+version\s+"?([^"\s]+)"?\s*$/);
|
|
118
|
-
if (
|
|
165
|
+
if (versionMatch) {
|
|
166
|
+
currentVersion = versionMatch[1];
|
|
167
|
+
for (const name of currentNames) {
|
|
168
|
+
addLockPackage(lockPackages, name, `yarn:${name}@${currentVersion}`, currentVersion);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (/^\s{2}dependencies:\s*$/.test(line)) {
|
|
173
|
+
inDependenciesBlock = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^\s{2}\S/.test(line) && !/^\s{2}dependencies:\s*$/.test(line)) {
|
|
177
|
+
inDependenciesBlock = false;
|
|
178
|
+
}
|
|
179
|
+
if (!inDependenciesBlock) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const dependencyMatch = line.match(/^\s{4}((?:@[^/]+\/)?[^"\s]+)\s+/);
|
|
183
|
+
if (!dependencyMatch) {
|
|
119
184
|
continue;
|
|
120
185
|
}
|
|
121
186
|
for (const name of currentNames) {
|
|
122
|
-
|
|
187
|
+
addDependencyNames(lockDependencies, name, [dependencyMatch[1]]);
|
|
123
188
|
}
|
|
124
189
|
}
|
|
125
|
-
|
|
190
|
+
addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
|
|
191
|
+
return {
|
|
192
|
+
lockPackages: toLockPackageRecord(lockPackages),
|
|
193
|
+
lockDependencies: toDependencyRecord(lockDependencies)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function extractPackageName(packagePath) {
|
|
197
|
+
if (!packagePath) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
|
|
201
|
+
if (!match) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return match[1];
|
|
126
205
|
}
|
|
127
206
|
function extractYarnEntryNames(entry) {
|
|
128
207
|
const names = new Set();
|
|
@@ -149,9 +228,25 @@ function addLockPackage(lockPackages, name, packagePath, version) {
|
|
|
149
228
|
instances.set(packagePath, { path: packagePath, version });
|
|
150
229
|
lockPackages.set(name, instances);
|
|
151
230
|
}
|
|
231
|
+
function addDependencyNames(lockDependencies, name, dependencies) {
|
|
232
|
+
if (dependencies.length === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const entry = lockDependencies.get(name) ?? new Set();
|
|
236
|
+
for (const dependency of dependencies) {
|
|
237
|
+
entry.add(dependency);
|
|
238
|
+
}
|
|
239
|
+
lockDependencies.set(name, entry);
|
|
240
|
+
}
|
|
152
241
|
function toLockPackageRecord(lockPackages) {
|
|
153
242
|
return Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
154
243
|
name,
|
|
155
244
|
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
156
245
|
]));
|
|
157
246
|
}
|
|
247
|
+
function toDependencyRecord(lockDependencies) {
|
|
248
|
+
return Object.fromEntries(Array.from(lockDependencies.entries()).map(([name, dependencies]) => [
|
|
249
|
+
name,
|
|
250
|
+
Array.from(dependencies).sort((left, right) => left.localeCompare(right))
|
|
251
|
+
]));
|
|
252
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
-
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
2
|
+
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, RiskTransitiveDependency, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
3
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
4
|
export { PluginManager } from "./core/plugin-manager.js";
|
|
5
5
|
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
@@ -31,8 +31,8 @@ export function renderConsoleReport(result) {
|
|
|
31
31
|
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
32
32
|
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
33
33
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
34
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
|
|
35
|
-
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
|
|
34
|
+
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
|
|
35
|
+
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
36
36
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
37
37
|
if (result.suggestions.length > 0) {
|
|
38
38
|
lines.push("");
|
|
@@ -43,6 +43,13 @@ export function renderConsoleReport(result) {
|
|
|
43
43
|
}
|
|
44
44
|
return lines.join("\n");
|
|
45
45
|
}
|
|
46
|
+
function formatTransitiveRiskSuffix(item) {
|
|
47
|
+
if (item.riskyTransitiveDeps.length === 0) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
|
|
51
|
+
return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
|
|
52
|
+
}
|
|
46
53
|
function summaryLine(label, count) {
|
|
47
54
|
const indicator = count === 0 ? "OK" : "WARN";
|
|
48
55
|
return `${indicator} ${label}: ${count}`;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export function renderDashboardReport(result) {
|
|
2
2
|
const topIssues = result.topIssues.map(renderTopIssue).join("");
|
|
3
3
|
const suggestions = result.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
|
4
|
+
const transitiveHotspots = result.risks
|
|
5
|
+
.filter((item) => item.riskyTransitiveDeps.length > 0)
|
|
6
|
+
.sort((left, right) => right.transitiveRiskScore - left.transitiveRiskScore)
|
|
7
|
+
.map(renderTransitiveHotspot)
|
|
8
|
+
.join("");
|
|
4
9
|
return [
|
|
5
10
|
"<!doctype html>",
|
|
6
11
|
'<html lang="en">',
|
|
@@ -14,9 +19,11 @@ export function renderDashboardReport(result) {
|
|
|
14
19
|
"header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
|
|
15
20
|
"h1{font-size:28px;margin:0 0 8px}",
|
|
16
21
|
"h2{font-size:18px;margin:0 0 12px}",
|
|
22
|
+
"h3{font-size:15px;margin:0 0 8px}",
|
|
17
23
|
".muted{color:#637083;font-size:13px}",
|
|
18
24
|
".score{font-size:48px;font-weight:700}",
|
|
19
25
|
".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
|
|
26
|
+
".split{display:grid;grid-template-columns:1.2fr .8fr;gap:16px;margin-top:16px}",
|
|
20
27
|
".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
|
|
21
28
|
".metric{font-size:28px;font-weight:700;margin-top:4px}",
|
|
22
29
|
".pass{color:#167a43}.fail{color:#b42318}",
|
|
@@ -24,7 +31,9 @@ export function renderDashboardReport(result) {
|
|
|
24
31
|
"li{margin:8px 0}",
|
|
25
32
|
".issue{margin-bottom:10px}",
|
|
26
33
|
".kind{font-size:12px;text-transform:uppercase;color:#637083}",
|
|
27
|
-
"
|
|
34
|
+
".hotspot{border-top:1px solid #e7ecf4;padding-top:12px;margin-top:12px}",
|
|
35
|
+
".path{font-family:Consolas,monospace;font-size:12px;background:#f3f6fb;border-radius:6px;padding:6px 8px;margin:6px 0}",
|
|
36
|
+
"@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.split{grid-template-columns:1fr}header{display:block}.score{font-size:40px}}",
|
|
28
37
|
"</style>",
|
|
29
38
|
"</head>",
|
|
30
39
|
"<body>",
|
|
@@ -42,20 +51,36 @@ export function renderDashboardReport(result) {
|
|
|
42
51
|
renderMetric("Outdated", result.outdated.length),
|
|
43
52
|
renderMetric("Risks", result.risks.length),
|
|
44
53
|
"</section>",
|
|
45
|
-
'<section class="
|
|
54
|
+
'<section class="split">',
|
|
55
|
+
'<div class="panel">',
|
|
46
56
|
"<h2>Policy</h2>",
|
|
47
57
|
`<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
|
|
48
58
|
result.policy.reasons.length > 0
|
|
49
59
|
? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
50
60
|
: '<p class="muted">No policy failures.</p>',
|
|
61
|
+
"</div>",
|
|
62
|
+
'<div class="panel">',
|
|
63
|
+
"<h2>Risk Snapshot</h2>",
|
|
64
|
+
`<div class="muted">${result.risks.filter((item) => item.riskyTransitiveDeps.length > 0).length} direct dependencies carry transitive risk.</div>`,
|
|
65
|
+
`<div class="metric">${result.risks.reduce((total, item) => total + item.transitiveRiskScore, 0)}</div>`,
|
|
66
|
+
'<div class="muted">Total transitive risk score</div>',
|
|
67
|
+
"</div>",
|
|
51
68
|
"</section>",
|
|
52
|
-
'<section class="
|
|
69
|
+
'<section class="split">',
|
|
70
|
+
'<div class="panel">',
|
|
53
71
|
"<h2>Top Issues</h2>",
|
|
54
72
|
topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
|
|
55
|
-
"</
|
|
56
|
-
'<
|
|
73
|
+
"</div>",
|
|
74
|
+
'<div class="panel">',
|
|
57
75
|
"<h2>Suggestions</h2>",
|
|
58
76
|
suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
|
|
77
|
+
"</div>",
|
|
78
|
+
"</section>",
|
|
79
|
+
'<section class="panel">',
|
|
80
|
+
"<h2>Transitive Risk Hotspots</h2>",
|
|
81
|
+
transitiveHotspots.length > 0
|
|
82
|
+
? transitiveHotspots
|
|
83
|
+
: '<p class="muted">No transitive risk hotspots found.</p>',
|
|
59
84
|
"</section>",
|
|
60
85
|
"</main>",
|
|
61
86
|
"</body>",
|
|
@@ -74,6 +99,20 @@ function renderTopIssue(item) {
|
|
|
74
99
|
"</li>"
|
|
75
100
|
].join("");
|
|
76
101
|
}
|
|
102
|
+
function renderTransitiveHotspot(item) {
|
|
103
|
+
return [
|
|
104
|
+
'<div class="hotspot">',
|
|
105
|
+
`<h3>${escapeHtml(item.name)} <span class="muted">score ${item.transitiveRiskScore}</span></h3>`,
|
|
106
|
+
`<div class="muted">${item.riskFactors.transitiveDependencyCount} transitive dependencies, ${item.riskyTransitiveDeps.length} risky transitive dependencies</div>`,
|
|
107
|
+
"<ul>",
|
|
108
|
+
item.riskyTransitiveDeps
|
|
109
|
+
.slice(0, 4)
|
|
110
|
+
.map((entry) => `<li><strong>${escapeHtml(entry.name)}</strong> [${escapeHtml(entry.trustScore.toUpperCase())}]<div>${escapeHtml(entry.reasons.join("; "))}</div>${entry.introducedByPaths.map((trace) => `<div class="path">${escapeHtml(trace)}</div>`).join("")}</li>`)
|
|
111
|
+
.join(""),
|
|
112
|
+
"</ul>",
|
|
113
|
+
"</div>"
|
|
114
|
+
].join("");
|
|
115
|
+
}
|
|
77
116
|
function escapeHtml(value) {
|
|
78
117
|
return value
|
|
79
118
|
.replace(/&/g, "&")
|
|
@@ -35,8 +35,8 @@ export function renderMarkdownReport(result) {
|
|
|
35
35
|
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
36
36
|
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
37
37
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
38
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
|
|
39
|
-
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
|
|
38
|
+
? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
|
|
39
|
+
: `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
|
|
40
40
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
41
41
|
if (result.suggestions.length > 0) {
|
|
42
42
|
lines.push("## Suggestions");
|
|
@@ -47,6 +47,13 @@ export function renderMarkdownReport(result) {
|
|
|
47
47
|
}
|
|
48
48
|
return lines.join("\n");
|
|
49
49
|
}
|
|
50
|
+
function formatTransitiveRiskSuffix(item) {
|
|
51
|
+
if (item.riskyTransitiveDeps.length === 0) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
|
|
55
|
+
return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
|
|
56
|
+
}
|
|
50
57
|
function appendSection(lines, title, items) {
|
|
51
58
|
if (items.length === 0) {
|
|
52
59
|
return;
|