dep-brain 0.9.0 → 1.0.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 +10 -0
- package/README.md +51 -4
- package/action.yml +116 -0
- package/depbrain.output.schema.json +0 -1
- package/dist/checks/outdated.js +16 -2
- package/dist/checks/risk.js +16 -2
- package/dist/cli.js +28 -4
- package/dist/core/analyzer.d.ts +7 -0
- package/dist/core/analyzer.js +87 -47
- package/dist/core/graph-builder.js +93 -1
- package/dist/index.d.ts +1 -1
- package/dist/reporters/sarif.d.ts +2 -0
- package/dist/reporters/sarif.js +126 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.0.0
|
|
6
|
+
|
|
7
|
+
- Stable v1 CLI and library release for explainable dependency intelligence.
|
|
8
|
+
- Added baseline mode with `--baseline <file>` to ignore existing dependency debt in CI.
|
|
9
|
+
- Added reusable GitHub Action metadata through `action.yml`.
|
|
10
|
+
- Added SARIF export support for code scanning workflows.
|
|
11
|
+
- Added stable JSON output fields for confidence, reason codes, explanations, recommendations, top issues, score breakdown, and workspace ownership summaries.
|
|
12
|
+
- Added bounded registry request parallelism for outdated and risk checks.
|
|
13
|
+
- Documented v1 readiness, release validation, and CI usage.
|
|
14
|
+
|
|
5
15
|
## 0.9.0
|
|
6
16
|
|
|
7
17
|
- Workspace-aware analysis for npm workspaces.
|
package/README.md
CHANGED
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
## What It Does
|
|
19
19
|
|
|
20
|
-
- Detect duplicate dependencies from
|
|
20
|
+
- Detect duplicate dependencies from npm, pnpm, and yarn lockfiles
|
|
21
21
|
- Detect likely unused dependencies from source imports and scripts
|
|
22
22
|
- Detect outdated packages
|
|
23
23
|
- Highlight dependency risk signals
|
|
24
24
|
- Score package trust using supply-chain metadata
|
|
25
25
|
- Generate a simple project health score
|
|
26
|
-
- Output reports in
|
|
26
|
+
- Output reports in console, JSON, Markdown, SARIF, and top-issues formats
|
|
27
|
+
- Gate CI with score and finding policies
|
|
28
|
+
- Compare new findings against a baseline report
|
|
27
29
|
|
|
28
30
|
The long-term goal is not just to list problems, but to answer:
|
|
29
31
|
|
|
@@ -31,18 +33,24 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
31
33
|
- Can I remove it safely?
|
|
32
34
|
- What should I fix first?
|
|
33
35
|
|
|
34
|
-
##
|
|
36
|
+
## v1 Features
|
|
35
37
|
|
|
36
38
|
- Duplicate dependency detection with lockfile instance tracking
|
|
37
39
|
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
38
40
|
- Outdated dependency reporting with `major`, `minor`, and `patch` classification
|
|
39
41
|
- Risk analysis based on npm package metadata
|
|
42
|
+
- Confidence scores, reason codes, explanations, and recommendations for findings
|
|
40
43
|
- Config loading from `depbrain.config.json`
|
|
41
44
|
- Ignore rules for noisy dependencies and checks
|
|
42
45
|
- CI-friendly policy evaluation with non-zero exit codes
|
|
43
46
|
- Workspace-aware analysis for npm workspaces
|
|
44
47
|
- Console reporting
|
|
45
48
|
- JSON output via `--json`
|
|
49
|
+
- Markdown output via `--md`
|
|
50
|
+
- SARIF output via `--sarif`
|
|
51
|
+
- Ranked top issues via `--top`
|
|
52
|
+
- Baseline mode via `--baseline`
|
|
53
|
+
- Reusable GitHub Action via `action.yml`
|
|
46
54
|
- Library entrypoint for programmatic use
|
|
47
55
|
|
|
48
56
|
## CLI Usage
|
|
@@ -61,6 +69,9 @@ npx dep-brain analyze --min-score 90 --fail-on-risks
|
|
|
61
69
|
npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
62
70
|
npx dep-brain analyze --md > depbrain.md
|
|
63
71
|
npx dep-brain analyze --json --out depbrain.json
|
|
72
|
+
npx dep-brain analyze --sarif --out depbrain.sarif
|
|
73
|
+
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
74
|
+
npx dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
|
|
64
75
|
npx dep-brain report --from depbrain.json --md --out depbrain.md
|
|
65
76
|
|
|
66
77
|
dep-brain config
|
|
@@ -138,6 +149,41 @@ dep-brain analyze --json --out depbrain.json
|
|
|
138
149
|
dep-brain report --from depbrain.json --md --out depbrain.md
|
|
139
150
|
```
|
|
140
151
|
|
|
152
|
+
## Baseline Mode
|
|
153
|
+
|
|
154
|
+
Baseline mode lets teams adopt `dep-brain` in existing repositories without failing CI for known dependency debt.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
dep-brain analyze --json --out depbrain-baseline.json
|
|
158
|
+
dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
## GitHub Action
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
name: Dependency Brain
|
|
167
|
+
|
|
168
|
+
on:
|
|
169
|
+
pull_request:
|
|
170
|
+
|
|
171
|
+
jobs:
|
|
172
|
+
dep-brain:
|
|
173
|
+
runs-on: ubuntu-latest
|
|
174
|
+
steps:
|
|
175
|
+
- uses: actions/checkout@v4
|
|
176
|
+
- uses: actions/setup-node@v4
|
|
177
|
+
with:
|
|
178
|
+
node-version: 22
|
|
179
|
+
- uses: prakashu51/dep-brain@v1
|
|
180
|
+
with:
|
|
181
|
+
format: sarif
|
|
182
|
+
out: depbrain.sarif
|
|
183
|
+
min-score: 85
|
|
184
|
+
fail-on-risks: "true"
|
|
185
|
+
```
|
|
186
|
+
|
|
141
187
|
## Config File
|
|
142
188
|
|
|
143
189
|
Create a `depbrain.config.json` file in the project root:
|
|
@@ -208,6 +254,7 @@ Examples:
|
|
|
208
254
|
dep-brain analyze --fail-on-unused
|
|
209
255
|
dep-brain analyze --min-score 85 --fail-on-risks
|
|
210
256
|
dep-brain analyze --config depbrain.config.json
|
|
257
|
+
dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
|
|
211
258
|
```
|
|
212
259
|
|
|
213
260
|
## Config Debugging
|
|
@@ -254,7 +301,7 @@ src/
|
|
|
254
301
|
|
|
255
302
|
## Product Direction
|
|
256
303
|
|
|
257
|
-
`dep-brain` is
|
|
304
|
+
`dep-brain` is in its `v1.0.0` production-ready CLI stage. The roadmap delivered through v1:
|
|
258
305
|
|
|
259
306
|
- `v0.6`: explainability and confidence scoring
|
|
260
307
|
- `v0.7`: safe removal guidance and actionable recommendations
|
package/action.yml
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
name: Dependency Brain
|
|
2
|
+
description: Run dep-brain dependency intelligence checks in GitHub Actions.
|
|
3
|
+
author: dep-brain
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
path:
|
|
7
|
+
description: Project path to analyze.
|
|
8
|
+
required: false
|
|
9
|
+
default: "."
|
|
10
|
+
format:
|
|
11
|
+
description: Output format. Use console, json, md, sarif, or top.
|
|
12
|
+
required: false
|
|
13
|
+
default: "console"
|
|
14
|
+
out:
|
|
15
|
+
description: Optional path to write the report.
|
|
16
|
+
required: false
|
|
17
|
+
default: ""
|
|
18
|
+
config:
|
|
19
|
+
description: Optional path to depbrain.config.json.
|
|
20
|
+
required: false
|
|
21
|
+
default: ""
|
|
22
|
+
baseline:
|
|
23
|
+
description: Optional baseline JSON report used to ignore existing findings.
|
|
24
|
+
required: false
|
|
25
|
+
default: ""
|
|
26
|
+
min-score:
|
|
27
|
+
description: Minimum project health score required to pass.
|
|
28
|
+
required: false
|
|
29
|
+
default: ""
|
|
30
|
+
fail-on-unused:
|
|
31
|
+
description: Fail when unused dependencies are found.
|
|
32
|
+
required: false
|
|
33
|
+
default: "false"
|
|
34
|
+
fail-on-outdated:
|
|
35
|
+
description: Fail when outdated dependencies are found.
|
|
36
|
+
required: false
|
|
37
|
+
default: "false"
|
|
38
|
+
fail-on-duplicates:
|
|
39
|
+
description: Fail when duplicate dependencies are found.
|
|
40
|
+
required: false
|
|
41
|
+
default: "false"
|
|
42
|
+
fail-on-risks:
|
|
43
|
+
description: Fail when risky dependencies are found.
|
|
44
|
+
required: false
|
|
45
|
+
default: "false"
|
|
46
|
+
|
|
47
|
+
runs:
|
|
48
|
+
using: composite
|
|
49
|
+
steps:
|
|
50
|
+
- name: Run dep-brain
|
|
51
|
+
shell: bash
|
|
52
|
+
env:
|
|
53
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
54
|
+
INPUT_FORMAT: ${{ inputs.format }}
|
|
55
|
+
INPUT_OUT: ${{ inputs.out }}
|
|
56
|
+
INPUT_CONFIG: ${{ inputs.config }}
|
|
57
|
+
INPUT_BASELINE: ${{ inputs.baseline }}
|
|
58
|
+
INPUT_MIN_SCORE: ${{ inputs.min-score }}
|
|
59
|
+
INPUT_FAIL_ON_UNUSED: ${{ inputs.fail-on-unused }}
|
|
60
|
+
INPUT_FAIL_ON_OUTDATED: ${{ inputs.fail-on-outdated }}
|
|
61
|
+
INPUT_FAIL_ON_DUPLICATES: ${{ inputs.fail-on-duplicates }}
|
|
62
|
+
INPUT_FAIL_ON_RISKS: ${{ inputs.fail-on-risks }}
|
|
63
|
+
run: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
|
|
66
|
+
args=("analyze" "$INPUT_PATH")
|
|
67
|
+
|
|
68
|
+
case "$INPUT_FORMAT" in
|
|
69
|
+
json) args+=("--json") ;;
|
|
70
|
+
md) args+=("--md") ;;
|
|
71
|
+
sarif) args+=("--sarif") ;;
|
|
72
|
+
top) args+=("--top") ;;
|
|
73
|
+
console) ;;
|
|
74
|
+
*)
|
|
75
|
+
echo "Unsupported format: $INPUT_FORMAT" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
|
|
80
|
+
if [ -n "$INPUT_OUT" ]; then
|
|
81
|
+
args+=("--out" "$INPUT_OUT")
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [ -n "$INPUT_CONFIG" ]; then
|
|
85
|
+
args+=("--config" "$INPUT_CONFIG")
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
if [ -n "$INPUT_BASELINE" ]; then
|
|
89
|
+
args+=("--baseline" "$INPUT_BASELINE")
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [ -n "$INPUT_MIN_SCORE" ]; then
|
|
93
|
+
args+=("--min-score" "$INPUT_MIN_SCORE")
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [ "$INPUT_FAIL_ON_UNUSED" = "true" ]; then
|
|
97
|
+
args+=("--fail-on-unused")
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [ "$INPUT_FAIL_ON_OUTDATED" = "true" ]; then
|
|
101
|
+
args+=("--fail-on-outdated")
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ "$INPUT_FAIL_ON_DUPLICATES" = "true" ]; then
|
|
105
|
+
args+=("--fail-on-duplicates")
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [ "$INPUT_FAIL_ON_RISKS" = "true" ]; then
|
|
109
|
+
args+=("--fail-on-risks")
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
node "$GITHUB_ACTION_PATH/dist/cli.js" "${args[@]}"
|
|
113
|
+
|
|
114
|
+
branding:
|
|
115
|
+
icon: activity
|
|
116
|
+
color: blue
|
package/dist/checks/outdated.js
CHANGED
|
@@ -5,7 +5,7 @@ export async function findOutdatedDependencies(graph, options = {}) {
|
|
|
5
5
|
...graph.dependencies,
|
|
6
6
|
...graph.devDependencies
|
|
7
7
|
};
|
|
8
|
-
const results = await
|
|
8
|
+
const results = await mapWithConcurrency(Object.entries(combined), 8, async ([name, current]) => {
|
|
9
9
|
const normalized = normalizeVersion(current);
|
|
10
10
|
const latest = await resolveLatestVersion(name);
|
|
11
11
|
if (!latest || latest === normalized) {
|
|
@@ -28,11 +28,25 @@ export async function findOutdatedDependencies(graph, options = {}) {
|
|
|
28
28
|
],
|
|
29
29
|
recommendation: buildOutdatedRecommendation(updateType)
|
|
30
30
|
};
|
|
31
|
-
})
|
|
31
|
+
});
|
|
32
32
|
return results
|
|
33
33
|
.filter((item) => item !== null)
|
|
34
34
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
35
35
|
}
|
|
36
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
37
|
+
const results = new Array(items.length);
|
|
38
|
+
let nextIndex = 0;
|
|
39
|
+
async function worker() {
|
|
40
|
+
while (nextIndex < items.length) {
|
|
41
|
+
const currentIndex = nextIndex;
|
|
42
|
+
nextIndex += 1;
|
|
43
|
+
results[currentIndex] = await mapper(items[currentIndex]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
47
|
+
await Promise.all(workers);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
36
50
|
function buildOutdatedRecommendation(updateType) {
|
|
37
51
|
return {
|
|
38
52
|
action: "upgrade",
|
package/dist/checks/risk.js
CHANGED
|
@@ -7,7 +7,7 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
7
7
|
...graph.dependencies,
|
|
8
8
|
...graph.devDependencies
|
|
9
9
|
});
|
|
10
|
-
const results = await
|
|
10
|
+
const results = await mapWithConcurrency(names, 8, async (name) => {
|
|
11
11
|
const metadata = await resolvePackageMetadata(name);
|
|
12
12
|
if (!metadata) {
|
|
13
13
|
return null;
|
|
@@ -31,11 +31,25 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
31
31
|
riskFactors: assessment.riskFactors,
|
|
32
32
|
recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
|
|
33
33
|
};
|
|
34
|
-
})
|
|
34
|
+
});
|
|
35
35
|
return results
|
|
36
36
|
.filter((item) => item !== null)
|
|
37
37
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
38
38
|
}
|
|
39
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
40
|
+
const results = new Array(items.length);
|
|
41
|
+
let nextIndex = 0;
|
|
42
|
+
async function worker() {
|
|
43
|
+
while (nextIndex < items.length) {
|
|
44
|
+
const currentIndex = nextIndex;
|
|
45
|
+
nextIndex += 1;
|
|
46
|
+
results[currentIndex] = await mapper(items[currentIndex]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
50
|
+
await Promise.all(workers);
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
39
53
|
export async function runRiskCheck(graph) {
|
|
40
54
|
const risks = await findRiskDependencies(graph);
|
|
41
55
|
return {
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { analyzeProject } from "./core/analyzer.js";
|
|
|
3
3
|
import { renderConsoleReport } from "./reporters/console.js";
|
|
4
4
|
import { renderJsonReport } from "./reporters/json.js";
|
|
5
5
|
import { renderMarkdownReport } from "./reporters/markdown.js";
|
|
6
|
+
import { renderSarifReport } from "./reporters/sarif.js";
|
|
6
7
|
import { promises as fs } from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
async function main() {
|
|
@@ -55,7 +56,9 @@ async function main() {
|
|
|
55
56
|
? renderTopIssuesReport(reportData)
|
|
56
57
|
: flags.has("--json")
|
|
57
58
|
? JSON.stringify(reportData, null, 2)
|
|
58
|
-
:
|
|
59
|
+
: flags.has("--sarif")
|
|
60
|
+
? renderSarifReport(reportData)
|
|
61
|
+
: renderMarkdownReport(reportData);
|
|
59
62
|
await writeOutput(output, optionValues.get("--out"));
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
@@ -100,15 +103,20 @@ async function main() {
|
|
|
100
103
|
}
|
|
101
104
|
try {
|
|
102
105
|
const cliConfig = buildCliConfig(flags, optionValues);
|
|
106
|
+
const baseline = await loadBaseline(optionValues.get("--baseline"));
|
|
103
107
|
const result = await analyzeProject({
|
|
104
108
|
rootDir: targetPath,
|
|
105
109
|
configPath: optionValues.get("--config"),
|
|
106
|
-
config: cliConfig
|
|
110
|
+
config: cliConfig,
|
|
111
|
+
baseline
|
|
107
112
|
});
|
|
108
113
|
let output;
|
|
109
114
|
if (flags.has("--json")) {
|
|
110
115
|
output = renderJsonReport(result);
|
|
111
116
|
}
|
|
117
|
+
else if (flags.has("--sarif")) {
|
|
118
|
+
output = renderSarifReport(result);
|
|
119
|
+
}
|
|
112
120
|
else if (flags.has("--top")) {
|
|
113
121
|
output = renderTopIssuesReport(result);
|
|
114
122
|
}
|
|
@@ -178,8 +186,8 @@ function printHelp() {
|
|
|
178
186
|
console.log("Dependency Brain");
|
|
179
187
|
console.log("");
|
|
180
188
|
console.log("Usage:");
|
|
181
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--top] [--out path] [--config path] [--
|
|
182
|
-
console.log(" dep-brain report --from <file> [--md] [--json] [--top] [--out path]");
|
|
189
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
190
|
+
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
|
|
183
191
|
console.log(" dep-brain config [path] [--config path]");
|
|
184
192
|
console.log(" dep-brain help");
|
|
185
193
|
console.log(" dep-brain --version");
|
|
@@ -187,8 +195,10 @@ function printHelp() {
|
|
|
187
195
|
console.log("Options:");
|
|
188
196
|
console.log(" --json Output JSON for analysis");
|
|
189
197
|
console.log(" --md Output Markdown report");
|
|
198
|
+
console.log(" --sarif Output SARIF format for Code Scanning");
|
|
190
199
|
console.log(" --top Output the ranked top issues only");
|
|
191
200
|
console.log(" --config <path> Path to depbrain.config.json");
|
|
201
|
+
console.log(" --baseline <path> Ignore findings already present in a baseline JSON report");
|
|
192
202
|
console.log(" --from <file> Read analysis JSON from file");
|
|
193
203
|
console.log(" --out <path> Write output to a file");
|
|
194
204
|
console.log(" --min-score <n> Minimum score required to pass");
|
|
@@ -210,6 +220,20 @@ async function loadPackageVersion() {
|
|
|
210
220
|
return null;
|
|
211
221
|
}
|
|
212
222
|
}
|
|
223
|
+
async function loadBaseline(baselinePath) {
|
|
224
|
+
if (!baselinePath) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const resolved = resolveUserPath(baselinePath);
|
|
228
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
229
|
+
const parsed = JSON.parse(raw);
|
|
230
|
+
return {
|
|
231
|
+
duplicates: Array.isArray(parsed.duplicates) ? parsed.duplicates : [],
|
|
232
|
+
unused: Array.isArray(parsed.unused) ? parsed.unused : [],
|
|
233
|
+
outdated: Array.isArray(parsed.outdated) ? parsed.outdated : [],
|
|
234
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : []
|
|
235
|
+
};
|
|
236
|
+
}
|
|
213
237
|
async function writeOutput(output, outPath) {
|
|
214
238
|
if (outPath) {
|
|
215
239
|
const resolved = resolveUserPath(outPath);
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ export interface AnalysisOptions {
|
|
|
3
3
|
rootDir?: string;
|
|
4
4
|
configPath?: string;
|
|
5
5
|
config?: DepBrainConfigOverrides;
|
|
6
|
+
baseline?: DepBrainBaseline;
|
|
7
|
+
}
|
|
8
|
+
export interface DepBrainBaseline {
|
|
9
|
+
duplicates?: Array<Partial<Pick<DuplicateDependency, "name">>>;
|
|
10
|
+
unused?: Array<Partial<Pick<UnusedDependency, "name" | "section" | "package">>>;
|
|
11
|
+
outdated?: Array<Partial<Pick<OutdatedDependency, "name" | "current" | "latest" | "package">>>;
|
|
12
|
+
risks?: Array<Partial<Pick<RiskDependency, "name" | "package">>>;
|
|
6
13
|
}
|
|
7
14
|
export interface DuplicateInstance {
|
|
8
15
|
path: string;
|
package/dist/core/analyzer.js
CHANGED
|
@@ -15,7 +15,9 @@ export async function analyzeProject(options = {}) {
|
|
|
15
15
|
const config = mergeConfig(loadedConfig, options.config);
|
|
16
16
|
const workspaces = await findWorkspacePackages(rootDir);
|
|
17
17
|
if (workspaces.length === 0) {
|
|
18
|
-
return analyzeSingleProject(rootDir, config
|
|
18
|
+
return analyzeSingleProject(rootDir, config, {
|
|
19
|
+
baseline: options.baseline
|
|
20
|
+
});
|
|
19
21
|
}
|
|
20
22
|
const rootGraph = await buildDependencyGraph(rootDir);
|
|
21
23
|
const duplicateCheck = await runDuplicateCheck(rootGraph);
|
|
@@ -33,11 +35,21 @@ export async function analyzeProject(options = {}) {
|
|
|
33
35
|
graph: await buildDependencyGraph(workspace.rootDir)
|
|
34
36
|
})));
|
|
35
37
|
const attributedDuplicates = addWorkspaceAttribution(duplicates, workspaceGraphs);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
38
|
+
const rawUnused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
|
|
39
|
+
const rawOutdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
|
|
40
|
+
const rawRisks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
|
|
41
|
+
const baselineFiltered = filterBaselineFindings({
|
|
42
|
+
duplicates: attributedDuplicates,
|
|
43
|
+
unused: rawUnused,
|
|
44
|
+
outdated: rawOutdated,
|
|
45
|
+
risks: rawRisks
|
|
46
|
+
}, options.baseline);
|
|
47
|
+
const activeDuplicates = baselineFiltered.duplicates;
|
|
48
|
+
const unused = baselineFiltered.unused;
|
|
49
|
+
const outdated = baselineFiltered.outdated;
|
|
50
|
+
const risks = baselineFiltered.risks;
|
|
39
51
|
const score = calculateHealthScore({
|
|
40
|
-
duplicates:
|
|
52
|
+
duplicates: activeDuplicates.length,
|
|
41
53
|
unused: unused.length,
|
|
42
54
|
outdated: outdated.length,
|
|
43
55
|
risks: risks.length,
|
|
@@ -47,17 +59,19 @@ export async function analyzeProject(options = {}) {
|
|
|
47
59
|
riskWeight: config.scoring.riskWeight
|
|
48
60
|
});
|
|
49
61
|
const scoreBreakdown = buildScoreBreakdown({
|
|
50
|
-
duplicates:
|
|
62
|
+
duplicates: activeDuplicates.length,
|
|
51
63
|
unused: unused.length,
|
|
52
64
|
outdated: outdated.length,
|
|
53
65
|
risks: risks.length
|
|
54
66
|
}, config);
|
|
55
67
|
const suggestions = [
|
|
56
|
-
...
|
|
68
|
+
...unused.map((item) => `Remove ${item.name} from ${item.section}`),
|
|
69
|
+
...activeDuplicates.map((item) => `Consider consolidating ${item.name} to one version`),
|
|
70
|
+
...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
|
|
57
71
|
].slice(0, config.report.maxSuggestions);
|
|
58
72
|
const policy = evaluatePolicy({
|
|
59
73
|
score,
|
|
60
|
-
duplicates:
|
|
74
|
+
duplicates: activeDuplicates.length,
|
|
61
75
|
unused: unused.length,
|
|
62
76
|
outdated: outdated.length,
|
|
63
77
|
risks: risks.length
|
|
@@ -69,18 +83,18 @@ export async function analyzeProject(options = {}) {
|
|
|
69
83
|
scoreBreakdown,
|
|
70
84
|
policy,
|
|
71
85
|
ownershipSummary: buildOwnershipSummary({
|
|
72
|
-
duplicates:
|
|
86
|
+
duplicates: activeDuplicates,
|
|
73
87
|
unused,
|
|
74
88
|
outdated,
|
|
75
89
|
risks
|
|
76
90
|
}),
|
|
77
|
-
duplicates:
|
|
91
|
+
duplicates: activeDuplicates,
|
|
78
92
|
unused,
|
|
79
93
|
outdated,
|
|
80
94
|
risks,
|
|
81
95
|
suggestions,
|
|
82
96
|
topIssues: buildTopIssues({
|
|
83
|
-
duplicates:
|
|
97
|
+
duplicates: activeDuplicates,
|
|
84
98
|
unused,
|
|
85
99
|
outdated,
|
|
86
100
|
risks
|
|
@@ -155,43 +169,49 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
155
169
|
const unused = mapUnusedIssues(issueGroups.unused);
|
|
156
170
|
const outdated = mapOutdatedIssues(issueGroups.outdated);
|
|
157
171
|
const risks = mapRiskIssues(issueGroups.risks);
|
|
172
|
+
const scopedUnused = options.packageName && options.packageName.trim().length > 0
|
|
173
|
+
? unused.map((item) => ({ ...item, package: options.packageName }))
|
|
174
|
+
: unused;
|
|
175
|
+
const scopedOutdated = options.packageName && options.packageName.trim().length > 0
|
|
176
|
+
? outdated.map((item) => ({ ...item, package: options.packageName }))
|
|
177
|
+
: outdated;
|
|
178
|
+
const scopedRisks = options.packageName && options.packageName.trim().length > 0
|
|
179
|
+
? risks.map((item) => ({ ...item, package: options.packageName }))
|
|
180
|
+
: risks;
|
|
181
|
+
const baselineFiltered = filterBaselineFindings({
|
|
182
|
+
duplicates,
|
|
183
|
+
unused: scopedUnused,
|
|
184
|
+
outdated: scopedOutdated,
|
|
185
|
+
risks: scopedRisks
|
|
186
|
+
}, options.baseline);
|
|
158
187
|
const score = calculateHealthScore({
|
|
159
|
-
duplicates: duplicates.length,
|
|
160
|
-
unused: unused.length,
|
|
161
|
-
outdated: outdated.length,
|
|
162
|
-
risks: risks.length,
|
|
188
|
+
duplicates: baselineFiltered.duplicates.length,
|
|
189
|
+
unused: baselineFiltered.unused.length,
|
|
190
|
+
outdated: baselineFiltered.outdated.length,
|
|
191
|
+
risks: baselineFiltered.risks.length,
|
|
163
192
|
duplicateWeight: config.scoring.duplicateWeight,
|
|
164
193
|
outdatedWeight: config.scoring.outdatedWeight,
|
|
165
194
|
unusedWeight: config.scoring.unusedWeight,
|
|
166
195
|
riskWeight: config.scoring.riskWeight
|
|
167
196
|
});
|
|
168
197
|
const scoreBreakdown = buildScoreBreakdown({
|
|
169
|
-
duplicates: duplicates.length,
|
|
170
|
-
unused: unused.length,
|
|
171
|
-
outdated: outdated.length,
|
|
172
|
-
risks: risks.length
|
|
198
|
+
duplicates: baselineFiltered.duplicates.length,
|
|
199
|
+
unused: baselineFiltered.unused.length,
|
|
200
|
+
outdated: baselineFiltered.outdated.length,
|
|
201
|
+
risks: baselineFiltered.risks.length
|
|
173
202
|
}, config);
|
|
174
203
|
const suggestions = [
|
|
175
|
-
...unused.map((item) => `Remove ${item.name} from ${item.section}`),
|
|
176
|
-
...duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
|
|
177
|
-
...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
|
|
204
|
+
...baselineFiltered.unused.map((item) => `Remove ${item.name} from ${item.section}`),
|
|
205
|
+
...baselineFiltered.duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
|
|
206
|
+
...baselineFiltered.outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
|
|
178
207
|
].slice(0, config.report.maxSuggestions);
|
|
179
208
|
const policy = evaluatePolicy({
|
|
180
209
|
score,
|
|
181
|
-
duplicates: duplicates.length,
|
|
182
|
-
unused: unused.length,
|
|
183
|
-
outdated: outdated.length,
|
|
184
|
-
risks: risks.length
|
|
210
|
+
duplicates: baselineFiltered.duplicates.length,
|
|
211
|
+
unused: baselineFiltered.unused.length,
|
|
212
|
+
outdated: baselineFiltered.outdated.length,
|
|
213
|
+
risks: baselineFiltered.risks.length
|
|
185
214
|
}, config);
|
|
186
|
-
const scopedUnused = options.packageName && options.packageName.trim().length > 0
|
|
187
|
-
? unused.map((item) => ({ ...item, package: options.packageName }))
|
|
188
|
-
: unused;
|
|
189
|
-
const scopedOutdated = options.packageName && options.packageName.trim().length > 0
|
|
190
|
-
? outdated.map((item) => ({ ...item, package: options.packageName }))
|
|
191
|
-
: outdated;
|
|
192
|
-
const scopedRisks = options.packageName && options.packageName.trim().length > 0
|
|
193
|
-
? risks.map((item) => ({ ...item, package: options.packageName }))
|
|
194
|
-
: risks;
|
|
195
215
|
return {
|
|
196
216
|
outputVersion: OUTPUT_VERSION,
|
|
197
217
|
rootDir,
|
|
@@ -199,21 +219,21 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
199
219
|
scoreBreakdown,
|
|
200
220
|
policy,
|
|
201
221
|
ownershipSummary: buildOwnershipSummary({
|
|
202
|
-
duplicates,
|
|
203
|
-
unused:
|
|
204
|
-
outdated:
|
|
205
|
-
risks:
|
|
222
|
+
duplicates: baselineFiltered.duplicates,
|
|
223
|
+
unused: baselineFiltered.unused,
|
|
224
|
+
outdated: baselineFiltered.outdated,
|
|
225
|
+
risks: baselineFiltered.risks
|
|
206
226
|
}),
|
|
207
|
-
duplicates,
|
|
208
|
-
unused:
|
|
209
|
-
outdated:
|
|
210
|
-
risks:
|
|
227
|
+
duplicates: baselineFiltered.duplicates,
|
|
228
|
+
unused: baselineFiltered.unused,
|
|
229
|
+
outdated: baselineFiltered.outdated,
|
|
230
|
+
risks: baselineFiltered.risks,
|
|
211
231
|
suggestions,
|
|
212
232
|
topIssues: buildTopIssues({
|
|
213
|
-
duplicates,
|
|
214
|
-
unused:
|
|
215
|
-
outdated:
|
|
216
|
-
risks:
|
|
233
|
+
duplicates: baselineFiltered.duplicates,
|
|
234
|
+
unused: baselineFiltered.unused,
|
|
235
|
+
outdated: baselineFiltered.outdated,
|
|
236
|
+
risks: baselineFiltered.risks
|
|
217
237
|
}),
|
|
218
238
|
config
|
|
219
239
|
};
|
|
@@ -497,6 +517,26 @@ function buildOwnershipSummary(input) {
|
|
|
497
517
|
risks: input.risks.length
|
|
498
518
|
};
|
|
499
519
|
}
|
|
520
|
+
function filterBaselineFindings(findings, baseline) {
|
|
521
|
+
if (!baseline) {
|
|
522
|
+
return findings;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
duplicates: findings.duplicates.filter((item) => !(baseline.duplicates ?? []).some((entry) => entry.name === item.name)),
|
|
526
|
+
unused: findings.unused.filter((item) => !(baseline.unused ?? []).some((entry) => entry.name === item.name &&
|
|
527
|
+
optionalMatches(entry.section, item.section) &&
|
|
528
|
+
optionalMatches(entry.package, item.package))),
|
|
529
|
+
outdated: findings.outdated.filter((item) => !(baseline.outdated ?? []).some((entry) => entry.name === item.name &&
|
|
530
|
+
optionalMatches(entry.current, item.current) &&
|
|
531
|
+
optionalMatches(entry.latest, item.latest) &&
|
|
532
|
+
optionalMatches(entry.package, item.package))),
|
|
533
|
+
risks: findings.risks.filter((item) => !(baseline.risks ?? []).some((entry) => entry.name === item.name &&
|
|
534
|
+
optionalMatches(entry.package, item.package)))
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function optionalMatches(expected, actual) {
|
|
538
|
+
return expected === undefined || expected === actual;
|
|
539
|
+
}
|
|
500
540
|
function normalizeConfidence(value) {
|
|
501
541
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
502
542
|
return 0.5;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
2
3
|
import { readJsonFile } from "../utils/file-parser.js";
|
|
3
4
|
export async function buildDependencyGraph(rootDir) {
|
|
4
5
|
const packageJsonPath = path.join(rootDir, "package.json");
|
|
5
6
|
const lockfilePath = path.join(rootDir, "package-lock.json");
|
|
7
|
+
const pnpmLockfilePath = path.join(rootDir, "pnpm-lock.yaml");
|
|
8
|
+
const yarnLockfilePath = path.join(rootDir, "yarn.lock");
|
|
6
9
|
const packageJson = await readJsonFile(packageJsonPath);
|
|
7
10
|
const lockPackages = new Map();
|
|
8
11
|
try {
|
|
@@ -29,13 +32,15 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
catch {
|
|
35
|
+
const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath);
|
|
32
36
|
return {
|
|
33
37
|
rootDir,
|
|
34
38
|
packageJsonPath,
|
|
39
|
+
lockfilePath: fallbackLockfile.lockfilePath,
|
|
35
40
|
dependencies: packageJson.dependencies ?? {},
|
|
36
41
|
devDependencies: packageJson.devDependencies ?? {},
|
|
37
42
|
scripts: packageJson.scripts ?? {},
|
|
38
|
-
lockPackages:
|
|
43
|
+
lockPackages: fallbackLockfile.lockPackages
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
46
|
return {
|
|
@@ -51,6 +56,30 @@ export async function buildDependencyGraph(rootDir) {
|
|
|
51
56
|
]))
|
|
52
57
|
};
|
|
53
58
|
}
|
|
59
|
+
async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(pnpmLockfilePath, "utf8");
|
|
62
|
+
return {
|
|
63
|
+
lockfilePath: pnpmLockfilePath,
|
|
64
|
+
lockPackages: parsePnpmLockfile(content)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Try yarn.lock below.
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const content = await fs.readFile(yarnLockfilePath, "utf8");
|
|
72
|
+
return {
|
|
73
|
+
lockfilePath: yarnLockfilePath,
|
|
74
|
+
lockPackages: parseYarnLockfile(content)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return {
|
|
79
|
+
lockPackages: {}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
function extractPackageName(packagePath) {
|
|
55
84
|
if (!packagePath) {
|
|
56
85
|
return null;
|
|
@@ -61,3 +90,66 @@ function extractPackageName(packagePath) {
|
|
|
61
90
|
}
|
|
62
91
|
return match[1];
|
|
63
92
|
}
|
|
93
|
+
function parsePnpmLockfile(content) {
|
|
94
|
+
const lockPackages = new Map();
|
|
95
|
+
for (const line of content.split(/\r?\n/)) {
|
|
96
|
+
const match = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
|
|
97
|
+
if (!match) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
addLockPackage(lockPackages, match[1], `pnpm-lock:${match[0].trim()}`, match[2]);
|
|
101
|
+
}
|
|
102
|
+
return toLockPackageRecord(lockPackages);
|
|
103
|
+
}
|
|
104
|
+
function parseYarnLockfile(content) {
|
|
105
|
+
const lockPackages = new Map();
|
|
106
|
+
let currentNames = [];
|
|
107
|
+
for (const line of content.split(/\r?\n/)) {
|
|
108
|
+
if (line.trim().length === 0 || line.startsWith("#")) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
112
|
+
currentNames = extractYarnEntryNames(line.slice(0, -1));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const versionMatch = line.match(/^\s+version\s+"?([^"\s]+)"?\s*$/);
|
|
116
|
+
if (!versionMatch) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const name of currentNames) {
|
|
120
|
+
addLockPackage(lockPackages, name, `yarn-lock:${name}@${versionMatch[1]}`, versionMatch[1]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return toLockPackageRecord(lockPackages);
|
|
124
|
+
}
|
|
125
|
+
function extractYarnEntryNames(entry) {
|
|
126
|
+
const names = new Set();
|
|
127
|
+
const unquoted = entry.replace(/^["']|["']$/g, "");
|
|
128
|
+
for (const selector of unquoted.split(/,\s*/)) {
|
|
129
|
+
const normalized = selector.replace(/^["']|["']$/g, "");
|
|
130
|
+
const withoutProtocol = normalized.replace(/@npm:/, "@");
|
|
131
|
+
if (withoutProtocol.startsWith("@")) {
|
|
132
|
+
const scoped = withoutProtocol.match(/^(@[^/]+\/[^@]+)/);
|
|
133
|
+
if (scoped) {
|
|
134
|
+
names.add(scoped[1]);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const unscoped = withoutProtocol.match(/^([^@]+)/);
|
|
139
|
+
if (unscoped?.[1]) {
|
|
140
|
+
names.add(unscoped[1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(names);
|
|
144
|
+
}
|
|
145
|
+
function addLockPackage(lockPackages, name, packagePath, version) {
|
|
146
|
+
const instances = lockPackages.get(name) ?? new Map();
|
|
147
|
+
instances.set(packagePath, { path: packagePath, version });
|
|
148
|
+
lockPackages.set(name, instances);
|
|
149
|
+
}
|
|
150
|
+
function toLockPackageRecord(lockPackages) {
|
|
151
|
+
return Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
|
|
152
|
+
name,
|
|
153
|
+
Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
|
|
154
|
+
]));
|
|
155
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
-
export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
2
|
+
export type { AnalysisOptions, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
3
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
4
|
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
5
5
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export function renderSarifReport(result) {
|
|
2
|
+
const rules = [
|
|
3
|
+
{
|
|
4
|
+
id: "dep-brain-unused",
|
|
5
|
+
shortDescription: { text: "Unused Dependency" },
|
|
6
|
+
fullDescription: { text: "A dependency was detected but does not appear to be used in the source code." },
|
|
7
|
+
helpUri: "https://github.com/prakashu51/dep-brain"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "dep-brain-duplicate",
|
|
11
|
+
shortDescription: { text: "Duplicate Dependency" },
|
|
12
|
+
fullDescription: { text: "Multiple versions of the same dependency exist in the lockfile." },
|
|
13
|
+
helpUri: "https://github.com/prakashu51/dep-brain"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "dep-brain-outdated",
|
|
17
|
+
shortDescription: { text: "Outdated Dependency" },
|
|
18
|
+
fullDescription: { text: "A newer version of the dependency is available." },
|
|
19
|
+
helpUri: "https://github.com/prakashu51/dep-brain"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "dep-brain-risk",
|
|
23
|
+
shortDescription: { text: "Risky Dependency" },
|
|
24
|
+
fullDescription: { text: "A dependency exhibits supply-chain risk signals." },
|
|
25
|
+
helpUri: "https://github.com/prakashu51/dep-brain"
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
const results = [];
|
|
29
|
+
const mapLevel = (priority) => {
|
|
30
|
+
switch (priority) {
|
|
31
|
+
case "high": return "error";
|
|
32
|
+
case "medium": return "warning";
|
|
33
|
+
case "low": return "note";
|
|
34
|
+
default: return "none";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
for (const item of result.unused) {
|
|
38
|
+
results.push({
|
|
39
|
+
ruleId: "dep-brain-unused",
|
|
40
|
+
level: mapLevel(item.recommendation.priority),
|
|
41
|
+
message: {
|
|
42
|
+
text: `[Unused] ${item.name}\n\n${item.recommendation.summary}\nReasons:\n- ${item.recommendation.reasons.join('\n- ')}`
|
|
43
|
+
},
|
|
44
|
+
locations: [
|
|
45
|
+
{
|
|
46
|
+
physicalLocation: {
|
|
47
|
+
artifactLocation: {
|
|
48
|
+
uri: item.package ? `packages/${item.package}/package.json` : "package.json"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const item of result.duplicates) {
|
|
56
|
+
results.push({
|
|
57
|
+
ruleId: "dep-brain-duplicate",
|
|
58
|
+
level: mapLevel(item.recommendation.priority),
|
|
59
|
+
message: {
|
|
60
|
+
text: `[Duplicate] ${item.name} (${item.versions.join(", ")})\n\n${item.recommendation.summary}`
|
|
61
|
+
},
|
|
62
|
+
locations: [
|
|
63
|
+
{
|
|
64
|
+
physicalLocation: {
|
|
65
|
+
artifactLocation: {
|
|
66
|
+
uri: "package-lock.json"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const item of result.outdated) {
|
|
74
|
+
results.push({
|
|
75
|
+
ruleId: "dep-brain-outdated",
|
|
76
|
+
level: mapLevel(item.recommendation.priority),
|
|
77
|
+
message: {
|
|
78
|
+
text: `[Outdated] ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})\n\n${item.recommendation.summary}`
|
|
79
|
+
},
|
|
80
|
+
locations: [
|
|
81
|
+
{
|
|
82
|
+
physicalLocation: {
|
|
83
|
+
artifactLocation: {
|
|
84
|
+
uri: item.package ? `packages/${item.package}/package.json` : "package.json"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const item of result.risks) {
|
|
92
|
+
results.push({
|
|
93
|
+
ruleId: "dep-brain-risk",
|
|
94
|
+
level: mapLevel(item.recommendation.priority),
|
|
95
|
+
message: {
|
|
96
|
+
text: `[Risk] ${item.name} (Trust: ${item.trustScore})\n\n${item.recommendation.summary}\nReasons:\n- ${item.recommendation.reasons.join('\n- ')}`
|
|
97
|
+
},
|
|
98
|
+
locations: [
|
|
99
|
+
{
|
|
100
|
+
physicalLocation: {
|
|
101
|
+
artifactLocation: {
|
|
102
|
+
uri: item.package ? `packages/${item.package}/package.json` : "package.json"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const sarif = {
|
|
110
|
+
version: "2.1.0",
|
|
111
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
112
|
+
runs: [
|
|
113
|
+
{
|
|
114
|
+
tool: {
|
|
115
|
+
driver: {
|
|
116
|
+
name: "Dependency Brain",
|
|
117
|
+
informationUri: "https://github.com/prakashu51/dep-brain",
|
|
118
|
+
rules
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
results
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
return JSON.stringify(sarif, null, 2);
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dep-brain",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI and library for dependency
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI and library for explainable dependency intelligence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"README.md",
|
|
14
14
|
"LICENSE",
|
|
15
15
|
"CHANGELOG.md",
|
|
16
|
+
"action.yml",
|
|
16
17
|
"depbrain.config.json",
|
|
17
18
|
"depbrain.config.schema.json",
|
|
18
19
|
"depbrain.output.schema.json"
|