dep-brain 1.2.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 +24 -0
- package/README.md +42 -28
- package/depbrain.config.json +6 -1
- package/depbrain.config.schema.json +6 -1
- package/depbrain.output.schema.json +22 -3
- package/dist/checks/risk.d.ts +3 -1
- package/dist/checks/risk.js +205 -35
- package/dist/cli.js +11 -3
- package/dist/core/analyzer.d.ts +12 -1
- package/dist/core/analyzer.js +56 -11
- package/dist/core/graph-builder.d.ts +1 -0
- package/dist/core/graph-builder.js +153 -58
- package/dist/core/plugin-manager.d.ts +9 -0
- package/dist/core/plugin-manager.js +142 -18
- package/dist/index.d.ts +2 -2
- package/dist/reporters/console.js +9 -2
- package/dist/reporters/dashboard.d.ts +2 -0
- package/dist/reporters/dashboard.js +123 -0
- package/dist/reporters/markdown.js +9 -2
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +12 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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
|
+
|
|
13
|
+
## 1.3.0
|
|
14
|
+
|
|
15
|
+
- Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
|
|
16
|
+
- Added built-in `license` plugin through `plugins.enabled: ["license"]`.
|
|
17
|
+
- Added configurable risk thresholds for stale release days, aging release days, low downloads, and trust score weights.
|
|
18
|
+
- Added `--dashboard` and `--dashboard-out` for static HTML dashboard generation.
|
|
19
|
+
- Updated starter config, schemas, README, and tests for v1.3 behavior.
|
|
20
|
+
|
|
21
|
+
## 1.2.0
|
|
22
|
+
|
|
23
|
+
- Added `PluginManager` with `preScan`, `postScan`, and `reportHook` lifecycle support.
|
|
24
|
+
- Added disabled-by-default plugin config through `plugins.enabled` and `plugins.paths`.
|
|
25
|
+
- Added `extensions` to analysis output so plugins can enrich results without breaking schema.
|
|
26
|
+
- Added future config slots for risk thresholds, dashboard output, and notification webhook env names.
|
|
27
|
+
- Added regression coverage for plugin hooks enriching `extensions`.
|
|
28
|
+
|
|
5
29
|
## 1.1.0
|
|
6
30
|
|
|
7
31
|
- Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
|
package/README.md
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
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
|
-
- Output reports in console, JSON, Markdown, SARIF, and top-issues formats
|
|
27
|
+
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
27
28
|
- Gate CI with score and finding policies
|
|
28
29
|
- Compare new findings against a baseline report
|
|
29
30
|
|
|
@@ -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
|
|
@@ -48,12 +50,14 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
48
50
|
- JSON output via `--json`
|
|
49
51
|
- Markdown output via `--md`
|
|
50
52
|
- SARIF output via `--sarif`
|
|
53
|
+
- Static HTML dashboard via `--dashboard`
|
|
51
54
|
- Ranked top issues via `--top`
|
|
52
55
|
- Baseline mode via `--baseline`
|
|
53
56
|
- Focused analysis via `--focus`
|
|
54
57
|
- Low-noise CI defaults via `--ci`
|
|
55
58
|
- Starter config generation via `dep-brain init`
|
|
56
59
|
- Reusable GitHub Action via `action.yml`
|
|
60
|
+
- Built-in `license` plugin and plugin diagnostics
|
|
57
61
|
- Library entrypoint for programmatic use
|
|
58
62
|
|
|
59
63
|
## CLI Usage
|
|
@@ -73,6 +77,8 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
|
73
77
|
npx dep-brain analyze --md > depbrain.md
|
|
74
78
|
npx dep-brain analyze --json --out depbrain.json
|
|
75
79
|
npx dep-brain analyze --sarif --out depbrain.sarif
|
|
80
|
+
npx dep-brain analyze --dashboard
|
|
81
|
+
npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
76
82
|
npx dep-brain analyze --focus duplicates
|
|
77
83
|
npx dep-brain analyze --ci
|
|
78
84
|
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
@@ -176,6 +182,28 @@ dep-brain analyze --top
|
|
|
176
182
|
|
|
177
183
|
Shows the highest-priority actionable findings first, including confidence and next-step guidance.
|
|
178
184
|
|
|
185
|
+
## Dashboard Output
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
dep-brain analyze --dashboard
|
|
189
|
+
dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
|
|
193
|
+
|
|
194
|
+
## Plugins
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"plugins": {
|
|
199
|
+
"enabled": ["license"],
|
|
200
|
+
"paths": ["./depbrain-plugin.mjs"]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
|
|
206
|
+
|
|
179
207
|
## Report From JSON
|
|
180
208
|
|
|
181
209
|
```bash
|
|
@@ -194,30 +222,6 @@ dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-ris
|
|
|
194
222
|
|
|
195
223
|
The baseline file is a normal JSON analysis report. Matching entries in `duplicates`, `unused`, `outdated`, and `risks` are ignored before score, policy, suggestions, and top issues are calculated.
|
|
196
224
|
|
|
197
|
-
## GitHub Action
|
|
198
|
-
|
|
199
|
-
```yaml
|
|
200
|
-
name: Dependency Brain
|
|
201
|
-
|
|
202
|
-
on:
|
|
203
|
-
pull_request:
|
|
204
|
-
|
|
205
|
-
jobs:
|
|
206
|
-
dep-brain:
|
|
207
|
-
runs-on: ubuntu-latest
|
|
208
|
-
steps:
|
|
209
|
-
- uses: actions/checkout@v4
|
|
210
|
-
- uses: actions/setup-node@v4
|
|
211
|
-
with:
|
|
212
|
-
node-version: 22
|
|
213
|
-
- uses: prakashu51/dep-brain@v1
|
|
214
|
-
with:
|
|
215
|
-
format: sarif
|
|
216
|
-
out: depbrain.sarif
|
|
217
|
-
min-score: 85
|
|
218
|
-
fail-on-risks: "true"
|
|
219
|
-
```
|
|
220
|
-
|
|
221
225
|
## Config File
|
|
222
226
|
|
|
223
227
|
Create a `depbrain.config.json` file in the project root:
|
|
@@ -239,12 +243,17 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
239
243
|
"maxSuggestions": 3
|
|
240
244
|
},
|
|
241
245
|
"plugins": {
|
|
242
|
-
"enabled": [],
|
|
246
|
+
"enabled": ["license"],
|
|
243
247
|
"paths": []
|
|
244
248
|
},
|
|
245
249
|
"risk": {
|
|
246
250
|
"transitiveBloatThreshold": 50,
|
|
247
|
-
"typosquattingDistanceThreshold": 2
|
|
251
|
+
"typosquattingDistanceThreshold": 2,
|
|
252
|
+
"staleReleaseDays": 730,
|
|
253
|
+
"agingReleaseDays": 365,
|
|
254
|
+
"lowDownloadThreshold": 1000,
|
|
255
|
+
"lowTrustWeightThreshold": 6,
|
|
256
|
+
"mediumTrustWeightThreshold": 3
|
|
248
257
|
},
|
|
249
258
|
"dashboard": {
|
|
250
259
|
"outputPath": "depbrain-dashboard.html"
|
|
@@ -283,6 +292,11 @@ Supported sections:
|
|
|
283
292
|
- `plugins.paths`
|
|
284
293
|
- `risk.transitiveBloatThreshold`
|
|
285
294
|
- `risk.typosquattingDistanceThreshold`
|
|
295
|
+
- `risk.staleReleaseDays`
|
|
296
|
+
- `risk.agingReleaseDays`
|
|
297
|
+
- `risk.lowDownloadThreshold`
|
|
298
|
+
- `risk.lowTrustWeightThreshold`
|
|
299
|
+
- `risk.mediumTrustWeightThreshold`
|
|
286
300
|
- `dashboard.outputPath`
|
|
287
301
|
- `notifications.slackWebhookEnv`
|
|
288
302
|
- `notifications.discordWebhookEnv`
|
|
@@ -367,7 +381,7 @@ src/
|
|
|
367
381
|
|
|
368
382
|
The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
369
383
|
|
|
370
|
-
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.
|
|
371
385
|
|
|
372
386
|
## Repository Notes
|
|
373
387
|
|
package/depbrain.config.json
CHANGED
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
},
|
|
26
26
|
"risk": {
|
|
27
27
|
"transitiveBloatThreshold": 50,
|
|
28
|
-
"typosquattingDistanceThreshold": 2
|
|
28
|
+
"typosquattingDistanceThreshold": 2,
|
|
29
|
+
"staleReleaseDays": 730,
|
|
30
|
+
"agingReleaseDays": 365,
|
|
31
|
+
"lowDownloadThreshold": 1000,
|
|
32
|
+
"lowTrustWeightThreshold": 6,
|
|
33
|
+
"mediumTrustWeightThreshold": 3
|
|
29
34
|
},
|
|
30
35
|
"dashboard": {
|
|
31
36
|
"outputPath": "depbrain-dashboard.html"
|
|
@@ -49,7 +49,12 @@
|
|
|
49
49
|
"additionalProperties": false,
|
|
50
50
|
"properties": {
|
|
51
51
|
"transitiveBloatThreshold": { "type": "number" },
|
|
52
|
-
"typosquattingDistanceThreshold": { "type": "number" }
|
|
52
|
+
"typosquattingDistanceThreshold": { "type": "number" },
|
|
53
|
+
"staleReleaseDays": { "type": "number" },
|
|
54
|
+
"agingReleaseDays": { "type": "number" },
|
|
55
|
+
"lowDownloadThreshold": { "type": "number" },
|
|
56
|
+
"lowTrustWeightThreshold": { "type": "number" },
|
|
57
|
+
"mediumTrustWeightThreshold": { "type": "number" }
|
|
53
58
|
}
|
|
54
59
|
},
|
|
55
60
|
"dashboard": {
|
|
@@ -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.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type { DependencyGraph } from "../core/graph-builder.js";
|
|
|
2
2
|
import type { RiskDependency } from "../core/analyzer.js";
|
|
3
3
|
import type { CheckResult } from "../core/types.js";
|
|
4
4
|
import { type PackageMetadata } from "../utils/npm-api.js";
|
|
5
|
+
import type { DepBrainConfig } from "../utils/config.js";
|
|
5
6
|
export interface RiskCheckOptions {
|
|
6
7
|
resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
|
|
8
|
+
thresholds?: DepBrainConfig["risk"];
|
|
7
9
|
}
|
|
8
10
|
export declare function findRiskDependencies(graph: DependencyGraph, options?: RiskCheckOptions): Promise<RiskDependency[]>;
|
|
9
|
-
export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
|
|
11
|
+
export declare function runRiskCheck(graph: DependencyGraph, options?: RiskCheckOptions): Promise<CheckResult>;
|
package/dist/checks/risk.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
|
-
const TWO_YEARS_IN_DAYS = 365 * 2;
|
|
3
|
-
const ONE_YEAR_IN_DAYS = 365;
|
|
4
2
|
export async function findRiskDependencies(graph, options = {}) {
|
|
5
3
|
const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
|
|
6
|
-
const
|
|
4
|
+
const thresholds = options.thresholds;
|
|
5
|
+
const allNames = Object.keys({
|
|
7
6
|
...graph.dependencies,
|
|
8
|
-
...graph.devDependencies
|
|
7
|
+
...graph.devDependencies,
|
|
8
|
+
...(graph.lockPackages ?? {})
|
|
9
9
|
});
|
|
10
|
-
const
|
|
10
|
+
const assessments = new Map();
|
|
11
|
+
const results = await mapWithConcurrency(allNames, 8, async (name) => {
|
|
11
12
|
const metadata = await resolvePackageMetadata(name);
|
|
12
13
|
if (!metadata) {
|
|
13
14
|
return null;
|
|
@@ -17,24 +18,25 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
17
18
|
: graph.devDependencies[name]
|
|
18
19
|
? "devDependencies"
|
|
19
20
|
: "unknown";
|
|
20
|
-
const assessment = assessRisk(metadata, dependencyType);
|
|
21
|
-
if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
21
|
+
const assessment = assessRisk(metadata, dependencyType, thresholds, 0);
|
|
24
22
|
return {
|
|
25
23
|
name,
|
|
26
|
-
|
|
27
|
-
confidence: assessment.confidence,
|
|
28
|
-
reasonCodes: assessment.reasonCodes,
|
|
29
|
-
explanation: assessment.reasons,
|
|
30
|
-
trustScore: assessment.trustScore,
|
|
31
|
-
riskFactors: assessment.riskFactors,
|
|
32
|
-
recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
|
|
24
|
+
assessment
|
|
33
25
|
};
|
|
34
26
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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));
|
|
38
40
|
}
|
|
39
41
|
async function mapWithConcurrency(items, limit, mapper) {
|
|
40
42
|
const results = new Array(items.length);
|
|
@@ -50,8 +52,8 @@ async function mapWithConcurrency(items, limit, mapper) {
|
|
|
50
52
|
await Promise.all(workers);
|
|
51
53
|
return results;
|
|
52
54
|
}
|
|
53
|
-
export async function runRiskCheck(graph) {
|
|
54
|
-
const risks = await findRiskDependencies(graph);
|
|
55
|
+
export async function runRiskCheck(graph, options = {}) {
|
|
56
|
+
const risks = await findRiskDependencies(graph, options);
|
|
55
57
|
return {
|
|
56
58
|
name: "risk",
|
|
57
59
|
summary: `${risks.length} risky dependencies found`,
|
|
@@ -70,27 +72,140 @@ export async function runRiskCheck(graph) {
|
|
|
70
72
|
name: item.name,
|
|
71
73
|
reasons: item.reasons,
|
|
72
74
|
trustScore: item.trustScore,
|
|
73
|
-
riskFactors: item.riskFactors
|
|
75
|
+
riskFactors: item.riskFactors,
|
|
76
|
+
transitiveRiskScore: item.transitiveRiskScore,
|
|
77
|
+
riskyTransitiveDeps: item.riskyTransitiveDeps
|
|
78
|
+
}
|
|
79
|
+
}))
|
|
80
|
+
};
|
|
81
|
+
}
|
|
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
|
+
});
|
|
74
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)
|
|
75
181
|
}))
|
|
182
|
+
.sort((left, right) => trustScoreWeight(right.trustScore) - trustScoreWeight(left.trustScore) ||
|
|
183
|
+
right.confidence - left.confidence ||
|
|
184
|
+
left.name.localeCompare(right.name)),
|
|
185
|
+
highestTrustScore
|
|
76
186
|
};
|
|
77
187
|
}
|
|
78
|
-
function assessRisk(metadata, dependencyType) {
|
|
188
|
+
function assessRisk(metadata, dependencyType, thresholds, transitiveDependencyCount) {
|
|
79
189
|
const reasons = [];
|
|
80
190
|
const reasonCodes = [];
|
|
81
191
|
let weight = 0;
|
|
82
|
-
|
|
83
|
-
|
|
192
|
+
const staleReleaseDays = thresholds?.staleReleaseDays ?? 730;
|
|
193
|
+
const agingReleaseDays = thresholds?.agingReleaseDays ?? 365;
|
|
194
|
+
const lowDownloadThreshold = thresholds?.lowDownloadThreshold ?? 1000;
|
|
195
|
+
const lowTrustWeightThreshold = thresholds?.lowTrustWeightThreshold ?? 6;
|
|
196
|
+
const mediumTrustWeightThreshold = thresholds?.mediumTrustWeightThreshold ?? 3;
|
|
197
|
+
if (metadata.daysSincePublish !== null && metadata.daysSincePublish > staleReleaseDays) {
|
|
198
|
+
reasons.push(`No release in over ${formatDays(staleReleaseDays)}`);
|
|
84
199
|
reasonCodes.push("stale_release");
|
|
85
200
|
weight += 3;
|
|
86
201
|
}
|
|
87
202
|
else if (metadata.daysSincePublish !== null &&
|
|
88
|
-
metadata.daysSincePublish >
|
|
89
|
-
reasons.push(
|
|
203
|
+
metadata.daysSincePublish > agingReleaseDays) {
|
|
204
|
+
reasons.push(`No release in over ${formatDays(agingReleaseDays)}`);
|
|
90
205
|
reasonCodes.push("aging_release");
|
|
91
206
|
weight += 2;
|
|
92
207
|
}
|
|
93
|
-
if (metadata.downloads !== null && metadata.downloads <
|
|
208
|
+
if (metadata.downloads !== null && metadata.downloads < lowDownloadThreshold) {
|
|
94
209
|
reasons.push("Low weekly download volume");
|
|
95
210
|
reasonCodes.push("low_download_volume");
|
|
96
211
|
weight += 2;
|
|
@@ -118,8 +233,13 @@ function assessRisk(metadata, dependencyType) {
|
|
|
118
233
|
weight += 1;
|
|
119
234
|
}
|
|
120
235
|
const confidence = reasons.length === 0 ? 0.5 : Math.min(0.99, 0.52 + weight * 0.07);
|
|
121
|
-
const trustScore = weight >=
|
|
236
|
+
const trustScore = weight >= lowTrustWeightThreshold
|
|
237
|
+
? "low"
|
|
238
|
+
: weight >= mediumTrustWeightThreshold
|
|
239
|
+
? "medium"
|
|
240
|
+
: "high";
|
|
122
241
|
return {
|
|
242
|
+
name: "",
|
|
123
243
|
confidence,
|
|
124
244
|
trustScore,
|
|
125
245
|
reasons,
|
|
@@ -131,10 +251,41 @@ function assessRisk(metadata, dependencyType) {
|
|
|
131
251
|
versionCount: metadata.versionCount,
|
|
132
252
|
recentReleaseCount: metadata.recentReleaseCount,
|
|
133
253
|
hasRepository: Boolean(metadata.repository),
|
|
134
|
-
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
|
|
135
277
|
}
|
|
136
278
|
};
|
|
137
279
|
}
|
|
280
|
+
function formatDays(days) {
|
|
281
|
+
if (days === 730) {
|
|
282
|
+
return "2 years";
|
|
283
|
+
}
|
|
284
|
+
if (days === 365) {
|
|
285
|
+
return "12 months";
|
|
286
|
+
}
|
|
287
|
+
return `${days} days`;
|
|
288
|
+
}
|
|
138
289
|
function shouldReportRisk(trustScore, dependencyType) {
|
|
139
290
|
if (trustScore === "high") {
|
|
140
291
|
return false;
|
|
@@ -144,14 +295,33 @@ function shouldReportRisk(trustScore, dependencyType) {
|
|
|
144
295
|
}
|
|
145
296
|
return true;
|
|
146
297
|
}
|
|
147
|
-
function buildRiskRecommendation(reasons, confidence, trustScore) {
|
|
298
|
+
function buildRiskRecommendation(reasons, confidence, trustScore, riskyTransitiveCount) {
|
|
148
299
|
return {
|
|
149
300
|
action: "review",
|
|
150
|
-
priority: trustScore === "low" || confidence >= 0.8
|
|
301
|
+
priority: trustScore === "low" || confidence >= 0.8 || riskyTransitiveCount >= 2
|
|
302
|
+
? "high"
|
|
303
|
+
: "medium",
|
|
151
304
|
safety: "caution",
|
|
152
|
-
summary:
|
|
153
|
-
?
|
|
154
|
-
:
|
|
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.",
|
|
155
310
|
reasons
|
|
156
311
|
};
|
|
157
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/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderConsoleReport } from "./reporters/console.js";
|
|
|
4
4
|
import { renderJsonReport } from "./reporters/json.js";
|
|
5
5
|
import { renderMarkdownReport } from "./reporters/markdown.js";
|
|
6
6
|
import { renderSarifReport } from "./reporters/sarif.js";
|
|
7
|
+
import { renderDashboardReport } from "./reporters/dashboard.js";
|
|
7
8
|
import { defaultConfig } from "./utils/config.js";
|
|
8
9
|
import { promises as fs } from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
@@ -59,7 +60,9 @@ async function main() {
|
|
|
59
60
|
? JSON.stringify(reportData, null, 2)
|
|
60
61
|
: flags.has("--sarif")
|
|
61
62
|
? renderSarifReport(reportData)
|
|
62
|
-
:
|
|
63
|
+
: flags.has("--dashboard")
|
|
64
|
+
? renderDashboardReport(reportData)
|
|
65
|
+
: renderMarkdownReport(reportData);
|
|
63
66
|
await writeOutput(output, optionValues.get("--out"));
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
@@ -149,6 +152,9 @@ async function main() {
|
|
|
149
152
|
: consoleOutput;
|
|
150
153
|
}
|
|
151
154
|
await writeOutput(output, optionValues.get("--out"));
|
|
155
|
+
if (flags.has("--dashboard")) {
|
|
156
|
+
await writeOutput(renderDashboardReport(result), optionValues.get("--dashboard-out") ?? result.config.dashboard.outputPath);
|
|
157
|
+
}
|
|
152
158
|
if (!result.policy.passed) {
|
|
153
159
|
process.exitCode = 1;
|
|
154
160
|
}
|
|
@@ -209,8 +215,8 @@ function printHelp() {
|
|
|
209
215
|
console.log("Dependency Brain");
|
|
210
216
|
console.log("");
|
|
211
217
|
console.log("Usage:");
|
|
212
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
213
|
-
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
|
|
218
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
219
|
+
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--dashboard] [--out path]");
|
|
214
220
|
console.log(" dep-brain config [path] [--config path]");
|
|
215
221
|
console.log(" dep-brain init [--out depbrain.config.json]");
|
|
216
222
|
console.log(" dep-brain help");
|
|
@@ -221,6 +227,8 @@ function printHelp() {
|
|
|
221
227
|
console.log(" --md Output Markdown report");
|
|
222
228
|
console.log(" --sarif Output SARIF format for Code Scanning");
|
|
223
229
|
console.log(" --top Output the ranked top issues only");
|
|
230
|
+
console.log(" --dashboard Write an HTML dashboard");
|
|
231
|
+
console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
|
|
224
232
|
console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
|
|
225
233
|
console.log(" --ci Apply low-noise CI defaults");
|
|
226
234
|
console.log(" --config <path> Path to depbrain.config.json");
|
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;
|