@rainy-updates/cli 0.5.1-rc.2 → 0.5.1
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 +84 -1
- package/README.md +8 -1
- package/dist/bin/cli.js +62 -12
- package/dist/commands/audit/fetcher.d.ts +6 -0
- package/dist/commands/audit/fetcher.js +79 -0
- package/dist/commands/audit/mapper.d.ts +16 -0
- package/dist/commands/audit/mapper.js +61 -0
- package/dist/commands/audit/parser.d.ts +3 -0
- package/dist/commands/audit/parser.js +87 -0
- package/dist/commands/audit/runner.d.ts +7 -0
- package/dist/commands/audit/runner.js +64 -0
- package/dist/commands/bisect/engine.d.ts +12 -0
- package/dist/commands/bisect/engine.js +89 -0
- package/dist/commands/bisect/oracle.d.ts +7 -0
- package/dist/commands/bisect/oracle.js +36 -0
- package/dist/commands/bisect/parser.d.ts +2 -0
- package/dist/commands/bisect/parser.js +73 -0
- package/dist/commands/bisect/runner.d.ts +6 -0
- package/dist/commands/bisect/runner.js +27 -0
- package/dist/commands/health/parser.d.ts +2 -0
- package/dist/commands/health/parser.js +90 -0
- package/dist/commands/health/runner.d.ts +7 -0
- package/dist/commands/health/runner.js +130 -0
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/policy.d.ts +4 -0
- package/dist/config/policy.js +2 -0
- package/dist/core/check.js +56 -3
- package/dist/core/fix-pr-batch.js +3 -2
- package/dist/core/fix-pr.js +19 -4
- package/dist/core/init-ci.js +3 -3
- package/dist/core/options.d.ts +10 -1
- package/dist/core/options.js +129 -13
- package/dist/core/summary.d.ts +1 -0
- package/dist/core/summary.js +11 -1
- package/dist/core/upgrade.js +10 -0
- package/dist/core/warm-cache.js +19 -1
- package/dist/output/format.js +4 -0
- package/dist/output/github.js +3 -0
- package/dist/registry/npm.d.ts +9 -2
- package/dist/registry/npm.js +87 -17
- package/dist/types/index.d.ts +83 -0
- package/dist/utils/lockfile.d.ts +5 -0
- package/dist/utils/lockfile.js +44 -0
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-02-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **New `audit` command**: Scan dependencies for known CVEs using [OSV.dev](https://osv.dev) (Google's open vulnerability database). Runs queries in parallel for all installed packages.
|
|
10
|
+
- `--severity critical|high|medium|low` — Filter by minimum severity level
|
|
11
|
+
- `--fix` — Print the minimum-secure-version `npm install` command to patch advisories
|
|
12
|
+
- `--dry-run` — Preview without side effects
|
|
13
|
+
- `--report json` — Machine-readable JSON output
|
|
14
|
+
- `--json-file <path>` — Write JSON report to file for CI pipelines
|
|
15
|
+
- Exit code `1` when vulnerabilities are found; `0` when clean.
|
|
16
|
+
|
|
17
|
+
- **New `health` command**: Surface stale, deprecated, and unmaintained packages before they become liabilities.
|
|
18
|
+
- `--stale 12m|180d|365` — Flag packages with no release in the given period (supports months and days)
|
|
19
|
+
- `--deprecated` / `--no-deprecated` — Control deprecated package detection
|
|
20
|
+
- `--alternatives` — Suggest active alternatives for deprecated packages
|
|
21
|
+
- `--report json` — Machine-readable JSON output
|
|
22
|
+
- Exit code `1` when flagged packages are found.
|
|
23
|
+
|
|
24
|
+
- **New `bisect` command**: Binary search across semver versions to find the exact version that introduced a failing test or breaking change.
|
|
25
|
+
- `rup bisect <package> --cmd "<test command>"` — Specify test oracle command
|
|
26
|
+
- `--range <start>..<end>` — Narrow the search to a specific version range
|
|
27
|
+
- `--dry-run` — Simulate without installing anything
|
|
28
|
+
- Exit code `1` when a breaking version is identified.
|
|
29
|
+
|
|
30
|
+
- **New CLI binary aliases for developer ergonomics**:
|
|
31
|
+
- `rup` — Ultra-short power-user alias (e.g., `rup ci`, `rup audit`)
|
|
32
|
+
- `rainy-up` — Human-friendly alias (e.g., `rainy-up check`)
|
|
33
|
+
- `rainy-updates` retained for backwards compatibility with CI scripts.
|
|
34
|
+
|
|
35
|
+
### Architecture
|
|
36
|
+
|
|
37
|
+
- `bisect`, `audit`, and `health` are fully isolated modules under `src/commands/`. They are lazy-loaded (dynamic `import()`) only when their command is invoked — zero startup cost penalty.
|
|
38
|
+
- `src/core/options.ts` now dispatches `bisect`, `audit`, and `health` to their isolated sub-parsers, keeping the command router clean and extensible.
|
|
39
|
+
- New type definitions: `AuditOptions`, `AuditResult`, `CveAdvisory`, `BisectOptions`, `BisectResult`, `HealthOptions`, `HealthResult`, `PackageHealthMetric`.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- CLI global help updated to list all 9 commands.
|
|
44
|
+
- Error messages now include `(rup)` in the binary identifier.
|
|
45
|
+
- `package.json` description updated to reflect DevOps-first positioning.
|
|
46
|
+
|
|
47
|
+
## [0.5.1-rc.4] - 2026-02-27
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- New registry and stream controls:
|
|
52
|
+
- `--registry-timeout-ms <n>`
|
|
53
|
+
- `--registry-retries <n>`
|
|
54
|
+
- `--stream`
|
|
55
|
+
- New lockfile execution control:
|
|
56
|
+
- `--lockfile-mode preserve|update|error`
|
|
57
|
+
- Policy extensions:
|
|
58
|
+
- package rule `target` override
|
|
59
|
+
- package rule `autofix` control for fix-PR flows
|
|
60
|
+
- New additive summary/output metadata:
|
|
61
|
+
- `streamedEvents`
|
|
62
|
+
- `policyOverridesApplied`
|
|
63
|
+
- `registryAuthFailure`
|
|
64
|
+
- `streamed_events` GitHub output key
|
|
65
|
+
- `policy_overrides_applied` GitHub output key
|
|
66
|
+
- `registry_auth_failures` GitHub output key
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- Registry client now supports configurable retry count and timeout defaults.
|
|
71
|
+
- Registry resolution now supports `.npmrc` auth token/basic auth parsing for scoped/private registries.
|
|
72
|
+
- Fix-PR automation now excludes updates with `autofix: false`.
|
|
73
|
+
- CI workflow templates generated by `init-ci` now include stream mode and registry control flags.
|
|
74
|
+
- Upgrade flow now enforces explicit lockfile policy semantics via `--lockfile-mode`.
|
|
75
|
+
|
|
76
|
+
### Tests
|
|
77
|
+
|
|
78
|
+
- Extended options parsing tests for registry/stream/lockfile flags.
|
|
79
|
+
- Extended policy tests for `target` and `autofix` rule behavior.
|
|
80
|
+
- Updated output and summary tests for additive metadata fields.
|
|
81
|
+
|
|
82
|
+
## [0.5.1-rc.3] - 2026-02-27
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- Resolved false dirty-worktree failures in `--fix-pr` flows caused by early PR report file creation.
|
|
87
|
+
- `deps-report.md` generation now runs after fix-PR git automation checks/operations.
|
|
88
|
+
|
|
5
89
|
## [0.5.1-rc.2] - 2026-02-27
|
|
6
90
|
|
|
7
91
|
### Added
|
|
@@ -175,7 +259,6 @@ All notable changes to this project are documented in this file.
|
|
|
175
259
|
- `--schedule weekly|daily|off`
|
|
176
260
|
- package-manager-aware install step generation (npm/pnpm)
|
|
177
261
|
|
|
178
|
-
|
|
179
262
|
## [0.4.0] - 2026-02-27
|
|
180
263
|
|
|
181
264
|
### Added
|
package/README.md
CHANGED
|
@@ -65,7 +65,9 @@ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --w
|
|
|
65
65
|
- Scans dependency groups: `dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`.
|
|
66
66
|
- Resolves versions per unique package to reduce duplicate network requests.
|
|
67
67
|
- Uses network concurrency controls and resilient retries.
|
|
68
|
+
- Supports explicit registry retry/timeout tuning (`--registry-retries`, `--registry-timeout-ms`).
|
|
68
69
|
- Supports stale-cache fallback when registry calls fail.
|
|
70
|
+
- Supports streamed progress output for long CI runs (`--stream`).
|
|
69
71
|
|
|
70
72
|
### Workspace support
|
|
71
73
|
|
|
@@ -80,6 +82,7 @@ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --w
|
|
|
80
82
|
- Apply global ignore patterns.
|
|
81
83
|
- Apply package-specific rules.
|
|
82
84
|
- Enforce max upgrade target per package (for safer rollout).
|
|
85
|
+
- Support per-package target override and fix-pr inclusion (`target`, `autofix`).
|
|
83
86
|
|
|
84
87
|
Example policy file:
|
|
85
88
|
|
|
@@ -87,7 +90,7 @@ Example policy file:
|
|
|
87
90
|
{
|
|
88
91
|
"ignore": ["@types/*", "eslint*"],
|
|
89
92
|
"packageRules": {
|
|
90
|
-
"react": { "maxTarget": "minor" },
|
|
93
|
+
"react": { "maxTarget": "minor", "target": "patch", "autofix": false },
|
|
91
94
|
"typescript": { "ignore": true }
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -155,7 +158,10 @@ Schedule:
|
|
|
155
158
|
- `--dep-kinds deps,dev,optional,peer`
|
|
156
159
|
- `--concurrency <n>`
|
|
157
160
|
- `--cache-ttl <seconds>`
|
|
161
|
+
- `--registry-timeout-ms <n>`
|
|
162
|
+
- `--registry-retries <n>`
|
|
158
163
|
- `--offline`
|
|
164
|
+
- `--stream`
|
|
159
165
|
- `--fail-on none|patch|minor|major|any`
|
|
160
166
|
- `--max-updates <n>`
|
|
161
167
|
- `--group-by none|name|scope|kind|risk`
|
|
@@ -175,6 +181,7 @@ Schedule:
|
|
|
175
181
|
- `--fix-branch <name>`
|
|
176
182
|
- `--fix-commit-message <text>`
|
|
177
183
|
- `--fix-dry-run`
|
|
184
|
+
- `--lockfile-mode preserve|update|error`
|
|
178
185
|
- `--no-pr-report`
|
|
179
186
|
- `--ci`
|
|
180
187
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -66,20 +66,42 @@ async function main() {
|
|
|
66
66
|
process.exitCode = 1;
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
if (parsed.
|
|
71
|
-
const
|
|
72
|
-
await
|
|
69
|
+
// ─── v0.5.1 commands: lazy-loaded, isolated from check pipeline ──────────
|
|
70
|
+
if (parsed.command === "bisect") {
|
|
71
|
+
const { runBisect } = await import("../commands/bisect/runner.js");
|
|
72
|
+
const result = await runBisect(parsed.options);
|
|
73
|
+
process.exitCode = result.breakingVersion ? 1 : 0;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (parsed.command === "audit") {
|
|
77
|
+
const { runAudit } = await import("../commands/audit/runner.js");
|
|
78
|
+
const result = await runAudit(parsed.options);
|
|
79
|
+
process.exitCode = result.advisories.length > 0 ? 1 : 0;
|
|
80
|
+
return;
|
|
73
81
|
}
|
|
74
|
-
if (parsed.
|
|
82
|
+
if (parsed.command === "health") {
|
|
83
|
+
const { runHealth } = await import("../commands/health/runner.js");
|
|
84
|
+
const result = await runHealth(parsed.options);
|
|
85
|
+
process.exitCode = result.totalFlagged > 0 ? 1 : 0;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const result = await runCommand(parsed);
|
|
89
|
+
if (parsed.options.fixPr &&
|
|
90
|
+
(parsed.command === "check" ||
|
|
91
|
+
parsed.command === "upgrade" ||
|
|
92
|
+
parsed.command === "ci")) {
|
|
75
93
|
result.summary.fixPrApplied = false;
|
|
76
|
-
result.summary.fixBranchName =
|
|
94
|
+
result.summary.fixBranchName =
|
|
95
|
+
parsed.options.fixBranch ?? "chore/rainy-updates";
|
|
77
96
|
result.summary.fixCommitSha = "";
|
|
78
97
|
result.summary.fixPrBranchesCreated = 0;
|
|
79
98
|
if (parsed.command === "ci") {
|
|
80
99
|
const batched = await applyFixPrBatches(parsed.options, result);
|
|
81
100
|
result.summary.fixPrApplied = batched.applied;
|
|
82
|
-
result.summary.fixBranchName =
|
|
101
|
+
result.summary.fixBranchName =
|
|
102
|
+
batched.branches[0] ??
|
|
103
|
+
parsed.options.fixBranch ??
|
|
104
|
+
"chore/rainy-updates";
|
|
83
105
|
result.summary.fixCommitSha = batched.commits[0] ?? "";
|
|
84
106
|
result.summary.fixPrBranchesCreated = batched.branches.length;
|
|
85
107
|
if (batched.branches.length > 1) {
|
|
@@ -87,25 +109,32 @@ async function main() {
|
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
else {
|
|
90
|
-
const fixResult = await applyFixPr(parsed.options, result,
|
|
112
|
+
const fixResult = await applyFixPr(parsed.options, result, []);
|
|
91
113
|
result.summary.fixPrApplied = fixResult.applied;
|
|
92
114
|
result.summary.fixBranchName = fixResult.branchName ?? "";
|
|
93
115
|
result.summary.fixCommitSha = fixResult.commitSha ?? "";
|
|
94
116
|
result.summary.fixPrBranchesCreated = fixResult.applied ? 1 : 0;
|
|
95
117
|
}
|
|
96
118
|
}
|
|
119
|
+
if (parsed.options.prReportFile) {
|
|
120
|
+
const markdown = renderPrReport(result);
|
|
121
|
+
await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
|
|
122
|
+
}
|
|
97
123
|
result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
98
124
|
const renderStartedAt = Date.now();
|
|
99
125
|
let rendered = renderResult(result, parsed.options.format);
|
|
100
126
|
result.summary.durationMs.render = Math.max(0, Date.now() - renderStartedAt);
|
|
101
|
-
if (parsed.options.format === "json" ||
|
|
127
|
+
if (parsed.options.format === "json" ||
|
|
128
|
+
parsed.options.format === "metrics") {
|
|
102
129
|
rendered = renderResult(result, parsed.options.format);
|
|
103
130
|
}
|
|
104
131
|
if (parsed.options.onlyChanged &&
|
|
105
132
|
result.updates.length === 0 &&
|
|
106
133
|
result.errors.length === 0 &&
|
|
107
134
|
result.warnings.length === 0 &&
|
|
108
|
-
(parsed.options.format === "table" ||
|
|
135
|
+
(parsed.options.format === "table" ||
|
|
136
|
+
parsed.options.format === "minimal" ||
|
|
137
|
+
parsed.options.format === "github")) {
|
|
109
138
|
rendered = "";
|
|
110
139
|
}
|
|
111
140
|
if (parsed.options.jsonFile) {
|
|
@@ -122,7 +151,7 @@ async function main() {
|
|
|
122
151
|
process.exitCode = resolveExitCode(result, result.summary.failReason);
|
|
123
152
|
}
|
|
124
153
|
catch (error) {
|
|
125
|
-
process.stderr.write(`rainy-updates: ${String(error)}\n`);
|
|
154
|
+
process.stderr.write(`rainy-updates (rup): ${String(error)}\n`);
|
|
126
155
|
process.exitCode = 2;
|
|
127
156
|
}
|
|
128
157
|
}
|
|
@@ -141,7 +170,10 @@ Options:
|
|
|
141
170
|
--reject <pattern>
|
|
142
171
|
--dep-kinds deps,dev,optional,peer
|
|
143
172
|
--concurrency <n>
|
|
173
|
+
--registry-timeout-ms <n>
|
|
174
|
+
--registry-retries <n>
|
|
144
175
|
--cache-ttl <seconds>
|
|
176
|
+
--stream
|
|
145
177
|
--policy-file <path>
|
|
146
178
|
--offline
|
|
147
179
|
--fix-pr
|
|
@@ -162,6 +194,7 @@ Options:
|
|
|
162
194
|
--cooldown-days <n>
|
|
163
195
|
--pr-limit <n>
|
|
164
196
|
--only-changed
|
|
197
|
+
--lockfile-mode preserve|update|error
|
|
165
198
|
--log-level error|warn|info|debug
|
|
166
199
|
--ci`;
|
|
167
200
|
}
|
|
@@ -177,8 +210,11 @@ Options:
|
|
|
177
210
|
--reject <pattern>
|
|
178
211
|
--dep-kinds deps,dev,optional,peer
|
|
179
212
|
--concurrency <n>
|
|
213
|
+
--registry-timeout-ms <n>
|
|
214
|
+
--registry-retries <n>
|
|
180
215
|
--cache-ttl <seconds>
|
|
181
216
|
--offline
|
|
217
|
+
--stream
|
|
182
218
|
--json-file <path>
|
|
183
219
|
--github-output <path>
|
|
184
220
|
--sarif-file <path>
|
|
@@ -197,12 +233,15 @@ Options:
|
|
|
197
233
|
--target patch|minor|major|latest
|
|
198
234
|
--policy-file <path>
|
|
199
235
|
--concurrency <n>
|
|
236
|
+
--registry-timeout-ms <n>
|
|
237
|
+
--registry-retries <n>
|
|
200
238
|
--fix-pr
|
|
201
239
|
--fix-branch <name>
|
|
202
240
|
--fix-commit-message <text>
|
|
203
241
|
--fix-dry-run
|
|
204
242
|
--fix-pr-no-checkout
|
|
205
243
|
--fix-pr-batch-size <n>
|
|
244
|
+
--lockfile-mode preserve|update|error
|
|
206
245
|
--no-pr-report
|
|
207
246
|
--json-file <path>
|
|
208
247
|
--pr-report-file <path>`;
|
|
@@ -222,6 +261,9 @@ Options:
|
|
|
222
261
|
--only-changed
|
|
223
262
|
--offline
|
|
224
263
|
--concurrency <n>
|
|
264
|
+
--registry-timeout-ms <n>
|
|
265
|
+
--registry-retries <n>
|
|
266
|
+
--stream
|
|
225
267
|
--fix-pr
|
|
226
268
|
--fix-branch <name>
|
|
227
269
|
--fix-commit-message <text>
|
|
@@ -235,6 +277,7 @@ Options:
|
|
|
235
277
|
--pr-report-file <path>
|
|
236
278
|
--fail-on none|patch|minor|major|any
|
|
237
279
|
--max-updates <n>
|
|
280
|
+
--lockfile-mode preserve|update|error
|
|
238
281
|
--log-level error|warn|info|debug
|
|
239
282
|
--ci`;
|
|
240
283
|
}
|
|
@@ -262,7 +305,7 @@ Options:
|
|
|
262
305
|
--dep-kinds deps,dev,optional,peer
|
|
263
306
|
--ci`;
|
|
264
307
|
}
|
|
265
|
-
return `rainy-updates <command> [options]
|
|
308
|
+
return `rainy-updates (rup / rainy-up) <command> [options]
|
|
266
309
|
|
|
267
310
|
Commands:
|
|
268
311
|
check Detect available updates
|
|
@@ -271,6 +314,9 @@ Commands:
|
|
|
271
314
|
warm-cache Warm local cache for fast/offline checks
|
|
272
315
|
init-ci Scaffold GitHub Actions workflow
|
|
273
316
|
baseline Save/check dependency baseline snapshots
|
|
317
|
+
audit Scan dependencies for CVEs (OSV.dev)
|
|
318
|
+
health Detect stale/deprecated/unmaintained packages
|
|
319
|
+
bisect Find which version of a dep introduced a failure
|
|
274
320
|
|
|
275
321
|
Global options:
|
|
276
322
|
--cwd <path>
|
|
@@ -299,8 +345,12 @@ Global options:
|
|
|
299
345
|
--no-pr-report
|
|
300
346
|
--log-level error|warn|info|debug
|
|
301
347
|
--concurrency <n>
|
|
348
|
+
--registry-timeout-ms <n>
|
|
349
|
+
--registry-retries <n>
|
|
302
350
|
--cache-ttl <seconds>
|
|
303
351
|
--offline
|
|
352
|
+
--stream
|
|
353
|
+
--lockfile-mode preserve|update|error
|
|
304
354
|
--ci
|
|
305
355
|
--help, -h
|
|
306
356
|
--version, -v`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CveAdvisory, AuditOptions } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches CVE advisories for all given package names in parallel.
|
|
4
|
+
* Uses OSV.dev as primary source.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fetchAdvisories(packageNames: string[], options: Pick<AuditOptions, "concurrency" | "registryTimeoutMs">): Promise<CveAdvisory[]>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { asyncPool } from "../../utils/async-pool.js";
|
|
2
|
+
const OSV_API = "https://api.osv.dev/v1/query";
|
|
3
|
+
const GITHUB_ADVISORY_API = "https://api.github.com/advisories";
|
|
4
|
+
/**
|
|
5
|
+
* Queries OSV.dev for advisories for a single npm package.
|
|
6
|
+
*/
|
|
7
|
+
async function queryOsv(packageName, timeoutMs) {
|
|
8
|
+
const body = JSON.stringify({
|
|
9
|
+
package: { name: packageName, ecosystem: "npm" },
|
|
10
|
+
});
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
15
|
+
response = await fetch(OSV_API, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body,
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
});
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
return [];
|
|
28
|
+
const data = (await response.json());
|
|
29
|
+
const advisories = [];
|
|
30
|
+
for (const vuln of data.vulns ?? []) {
|
|
31
|
+
const cveId = vuln.id ?? "UNKNOWN";
|
|
32
|
+
const rawSeverity = (vuln.database_specific?.severity ?? "medium").toLowerCase();
|
|
33
|
+
const severity = (["critical", "high", "medium", "low"].includes(rawSeverity)
|
|
34
|
+
? rawSeverity
|
|
35
|
+
: "medium");
|
|
36
|
+
let patchedVersion = null;
|
|
37
|
+
let vulnerableRange = "*";
|
|
38
|
+
for (const affected of vuln.affected ?? []) {
|
|
39
|
+
if (affected.package?.name !== packageName)
|
|
40
|
+
continue;
|
|
41
|
+
for (const range of affected.ranges ?? []) {
|
|
42
|
+
const fixedEvent = range.events?.find((e) => e.fixed);
|
|
43
|
+
if (fixedEvent?.fixed) {
|
|
44
|
+
patchedVersion = fixedEvent.fixed;
|
|
45
|
+
const introducedEvent = range.events?.find((e) => e.introduced);
|
|
46
|
+
vulnerableRange = introducedEvent?.introduced
|
|
47
|
+
? `>=${introducedEvent.introduced} <${patchedVersion}`
|
|
48
|
+
: `<${patchedVersion}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
advisories.push({
|
|
53
|
+
cveId,
|
|
54
|
+
packageName,
|
|
55
|
+
severity,
|
|
56
|
+
vulnerableRange,
|
|
57
|
+
patchedVersion,
|
|
58
|
+
title: vuln.summary ?? cveId,
|
|
59
|
+
url: vuln.references?.[0]?.url ?? `https://osv.dev/vulnerability/${cveId}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return advisories;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fetches CVE advisories for all given package names in parallel.
|
|
66
|
+
* Uses OSV.dev as primary source.
|
|
67
|
+
*/
|
|
68
|
+
export async function fetchAdvisories(packageNames, options) {
|
|
69
|
+
const tasks = packageNames.map((name) => () => queryOsv(name, options.registryTimeoutMs));
|
|
70
|
+
const results = await asyncPool(options.concurrency, tasks);
|
|
71
|
+
const advisories = [];
|
|
72
|
+
for (const r of results) {
|
|
73
|
+
if (!(r instanceof Error)) {
|
|
74
|
+
for (const adv of r)
|
|
75
|
+
advisories.push(adv);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return advisories;
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CveAdvisory, AuditSeverity } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Filters advisories by minimum severity level.
|
|
4
|
+
* e.g. --severity high → keeps critical and high.
|
|
5
|
+
*/
|
|
6
|
+
export declare function filterBySeverity(advisories: CveAdvisory[], minSeverity: AuditSeverity | undefined): CveAdvisory[];
|
|
7
|
+
/**
|
|
8
|
+
* For each advisory that has a known patchedVersion,
|
|
9
|
+
* produces a sorted, deduplicated map of package → minimum secure version.
|
|
10
|
+
* Used by --fix to determine what version to update to.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPatchMap(advisories: CveAdvisory[]): Map<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Renders audit advisories as a formatted table string for terminal output.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderAuditTable(advisories: CveAdvisory[]): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const SEVERITY_RANK = {
|
|
2
|
+
critical: 4,
|
|
3
|
+
high: 3,
|
|
4
|
+
medium: 2,
|
|
5
|
+
low: 1,
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Filters advisories by minimum severity level.
|
|
9
|
+
* e.g. --severity high → keeps critical and high.
|
|
10
|
+
*/
|
|
11
|
+
export function filterBySeverity(advisories, minSeverity) {
|
|
12
|
+
if (!minSeverity)
|
|
13
|
+
return advisories;
|
|
14
|
+
const minRank = SEVERITY_RANK[minSeverity];
|
|
15
|
+
return advisories.filter((a) => SEVERITY_RANK[a.severity] >= minRank);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* For each advisory that has a known patchedVersion,
|
|
19
|
+
* produces a sorted, deduplicated map of package → minimum secure version.
|
|
20
|
+
* Used by --fix to determine what version to update to.
|
|
21
|
+
*/
|
|
22
|
+
export function buildPatchMap(advisories) {
|
|
23
|
+
const patchMap = new Map();
|
|
24
|
+
for (const advisory of advisories) {
|
|
25
|
+
if (!advisory.patchedVersion)
|
|
26
|
+
continue;
|
|
27
|
+
const existing = patchMap.get(advisory.packageName);
|
|
28
|
+
if (!existing || advisory.patchedVersion > existing) {
|
|
29
|
+
patchMap.set(advisory.packageName, advisory.patchedVersion);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return patchMap;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Renders audit advisories as a formatted table string for terminal output.
|
|
36
|
+
*/
|
|
37
|
+
export function renderAuditTable(advisories) {
|
|
38
|
+
if (advisories.length === 0) {
|
|
39
|
+
return "✔ No vulnerabilities found.\n";
|
|
40
|
+
}
|
|
41
|
+
const SEVERITY_ICON = {
|
|
42
|
+
critical: "🔴 CRITICAL",
|
|
43
|
+
high: "🟠 HIGH ",
|
|
44
|
+
medium: "🟡 MEDIUM ",
|
|
45
|
+
low: "⚪ LOW ",
|
|
46
|
+
};
|
|
47
|
+
const sorted = [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
48
|
+
const lines = [
|
|
49
|
+
`Found ${advisories.length} vulnerability${advisories.length === 1 ? "" : "ies"}:\n`,
|
|
50
|
+
"Package".padEnd(30) + "Severity".padEnd(20) + "CVE".padEnd(22) + "Patch",
|
|
51
|
+
"─".repeat(90),
|
|
52
|
+
];
|
|
53
|
+
for (const adv of sorted) {
|
|
54
|
+
const name = adv.packageName.slice(0, 28).padEnd(30);
|
|
55
|
+
const sev = SEVERITY_ICON[adv.severity].padEnd(20);
|
|
56
|
+
const cve = adv.cveId.slice(0, 20).padEnd(22);
|
|
57
|
+
const patch = adv.patchedVersion ? `→ ${adv.patchedVersion}` : "no patch";
|
|
58
|
+
lines.push(`${name}${sev}${cve}${patch}`);
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
const SEVERITY_LEVELS = ["critical", "high", "medium", "low"];
|
|
4
|
+
export function parseSeverity(value) {
|
|
5
|
+
if (SEVERITY_LEVELS.includes(value)) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
throw new Error(`--severity must be critical, high, medium, or low. Got: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
export function parseAuditArgs(args) {
|
|
11
|
+
const options = {
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
workspace: false,
|
|
14
|
+
severity: undefined,
|
|
15
|
+
fix: false,
|
|
16
|
+
dryRun: false,
|
|
17
|
+
reportFormat: "table",
|
|
18
|
+
jsonFile: undefined,
|
|
19
|
+
concurrency: 16,
|
|
20
|
+
registryTimeoutMs: 8000,
|
|
21
|
+
};
|
|
22
|
+
let index = 0;
|
|
23
|
+
while (index < args.length) {
|
|
24
|
+
const current = args[index];
|
|
25
|
+
const next = args[index + 1];
|
|
26
|
+
if (current === "--cwd" && next) {
|
|
27
|
+
options.cwd = path.resolve(next);
|
|
28
|
+
index += 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (current === "--cwd")
|
|
32
|
+
throw new Error("Missing value for --cwd");
|
|
33
|
+
if (current === "--workspace") {
|
|
34
|
+
options.workspace = true;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (current === "--severity" && next) {
|
|
39
|
+
options.severity = parseSeverity(next);
|
|
40
|
+
index += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (current === "--severity")
|
|
44
|
+
throw new Error("Missing value for --severity");
|
|
45
|
+
if (current === "--fix") {
|
|
46
|
+
options.fix = true;
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (current === "--dry-run") {
|
|
51
|
+
options.dryRun = true;
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (current === "--report" && next) {
|
|
56
|
+
if (next !== "table" && next !== "json") {
|
|
57
|
+
throw new Error("--report must be table or json");
|
|
58
|
+
}
|
|
59
|
+
options.reportFormat = next;
|
|
60
|
+
index += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (current === "--report")
|
|
64
|
+
throw new Error("Missing value for --report");
|
|
65
|
+
if (current === "--json-file" && next) {
|
|
66
|
+
options.jsonFile = path.resolve(options.cwd, next);
|
|
67
|
+
index += 2;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (current === "--json-file")
|
|
71
|
+
throw new Error("Missing value for --json-file");
|
|
72
|
+
if (current === "--concurrency" && next) {
|
|
73
|
+
const parsed = Number(next);
|
|
74
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
75
|
+
throw new Error("--concurrency must be a positive integer");
|
|
76
|
+
options.concurrency = parsed;
|
|
77
|
+
index += 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (current === "--concurrency")
|
|
81
|
+
throw new Error("Missing value for --concurrency");
|
|
82
|
+
if (current.startsWith("-"))
|
|
83
|
+
throw new Error(`Unknown audit option: ${current}`);
|
|
84
|
+
throw new Error(`Unexpected audit argument: ${current}`);
|
|
85
|
+
}
|
|
86
|
+
return options;
|
|
87
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuditOptions, AuditResult } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Entry point for `rup audit`. Lazy-loaded by cli.ts.
|
|
4
|
+
* Discovers packages, fetches CVE advisories, filters by severity, and
|
|
5
|
+
* optionally applies minimum-secure-version patches.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runAudit(options: AuditOptions): Promise<AuditResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { collectDependencies, readManifest, } from "../../parsers/package-json.js";
|
|
2
|
+
import { discoverPackageDirs } from "../../workspace/discover.js";
|
|
3
|
+
import { writeFileAtomic } from "../../utils/io.js";
|
|
4
|
+
import { stableStringify } from "../../utils/stable-json.js";
|
|
5
|
+
import { fetchAdvisories } from "./fetcher.js";
|
|
6
|
+
import { filterBySeverity, buildPatchMap, renderAuditTable } from "./mapper.js";
|
|
7
|
+
/**
|
|
8
|
+
* Entry point for `rup audit`. Lazy-loaded by cli.ts.
|
|
9
|
+
* Discovers packages, fetches CVE advisories, filters by severity, and
|
|
10
|
+
* optionally applies minimum-secure-version patches.
|
|
11
|
+
*/
|
|
12
|
+
export async function runAudit(options) {
|
|
13
|
+
const result = {
|
|
14
|
+
advisories: [],
|
|
15
|
+
autoFixable: 0,
|
|
16
|
+
errors: [],
|
|
17
|
+
warnings: [],
|
|
18
|
+
};
|
|
19
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
20
|
+
// Collect all unique package names
|
|
21
|
+
const packageNames = new Set();
|
|
22
|
+
for (const dir of packageDirs) {
|
|
23
|
+
let manifest;
|
|
24
|
+
try {
|
|
25
|
+
manifest = await readManifest(dir);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
result.errors.push(`Failed to read package.json in ${dir}: ${String(error)}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const deps = collectDependencies(manifest, [
|
|
32
|
+
"dependencies",
|
|
33
|
+
"devDependencies",
|
|
34
|
+
"optionalDependencies",
|
|
35
|
+
]);
|
|
36
|
+
for (const dep of deps) {
|
|
37
|
+
packageNames.add(dep.name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (packageNames.size === 0) {
|
|
41
|
+
result.warnings.push("No dependencies found to audit.");
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write(`[audit] Querying OSV.dev for ${packageNames.size} packages...\n`);
|
|
45
|
+
let advisories = await fetchAdvisories([...packageNames], {
|
|
46
|
+
concurrency: options.concurrency,
|
|
47
|
+
registryTimeoutMs: options.registryTimeoutMs,
|
|
48
|
+
});
|
|
49
|
+
advisories = filterBySeverity(advisories, options.severity);
|
|
50
|
+
result.advisories = advisories;
|
|
51
|
+
result.autoFixable = advisories.filter((a) => a.patchedVersion !== null).length;
|
|
52
|
+
if (options.reportFormat === "table" || !options.jsonFile) {
|
|
53
|
+
process.stdout.write(renderAuditTable(advisories) + "\n");
|
|
54
|
+
}
|
|
55
|
+
if (options.jsonFile) {
|
|
56
|
+
await writeFileAtomic(options.jsonFile, stableStringify({ advisories, errors: result.errors, warnings: result.warnings }, 2) + "\n");
|
|
57
|
+
process.stderr.write(`[audit] JSON report written to ${options.jsonFile}\n`);
|
|
58
|
+
}
|
|
59
|
+
if (options.fix && !options.dryRun && result.autoFixable > 0) {
|
|
60
|
+
const patchMap = buildPatchMap(advisories);
|
|
61
|
+
process.stderr.write(`[audit] --fix: ${patchMap.size} packages have available patches. Apply with: npm install ${[...patchMap.entries()].map(([n, v]) => `${n}@${v}`).join(" ")}\n`);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BisectOptions, BisectResult } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Binary search engine for dependency bisecting.
|
|
4
|
+
* Given a sorted list of versions, finds the exact version that causes
|
|
5
|
+
* a test oracle to switch from "good" → "bad".
|
|
6
|
+
*/
|
|
7
|
+
export declare function bisectVersions(versions: string[], options: BisectOptions): Promise<BisectResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Fetches available versions for a package from registry/cache,
|
|
10
|
+
* optionally filtered to a user-specified range, sorted ascending.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchBisectVersions(options: BisectOptions): Promise<string[]>;
|