@4da/mcp-server 6.2.4 → 6.2.5

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/README.md +22 -6
  3. package/dist/db.d.ts +38 -0
  4. package/dist/db.js +78 -23
  5. package/dist/doctor.js +6 -3
  6. package/dist/embeddings.d.ts +1 -1
  7. package/dist/embeddings.js +16 -14
  8. package/dist/index.js +73 -103
  9. package/dist/live/advisory-groups.d.ts +68 -0
  10. package/dist/live/advisory-groups.js +131 -0
  11. package/dist/live/cache.js +11 -3
  12. package/dist/live/cargo-platform.d.ts +6 -0
  13. package/dist/live/cargo-platform.js +8 -2
  14. package/dist/live/changelog.js +38 -6
  15. package/dist/live/dependency-set.d.ts +9 -1
  16. package/dist/live/dependency-set.js +10 -2
  17. package/dist/live/fix-paths.d.ts +258 -9
  18. package/dist/live/fix-paths.js +790 -45
  19. package/dist/live/github-releases.js +3 -0
  20. package/dist/live/held-dependencies.d.ts +93 -0
  21. package/dist/live/held-dependencies.js +212 -0
  22. package/dist/live/held-targets.d.ts +30 -0
  23. package/dist/live/held-targets.js +107 -0
  24. package/dist/live/http-utils.js +13 -3
  25. package/dist/live/index.d.ts +7 -4
  26. package/dist/live/index.js +24 -19
  27. package/dist/live/install-state.js +6 -2
  28. package/dist/live/js-lockfile-readers.d.ts +8 -1
  29. package/dist/live/js-lockfile-readers.js +43 -6
  30. package/dist/live/lockfile-parents.d.ts +15 -0
  31. package/dist/live/lockfile-parents.js +85 -10
  32. package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
  33. package/dist/live/lockfile-parsers-pygo.js +230 -12
  34. package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
  35. package/dist/live/lockfile-parsers-rbphp.js +123 -0
  36. package/dist/live/lockfile-parsers.d.ts +6 -0
  37. package/dist/live/lockfile-parsers.js +116 -4
  38. package/dist/live/lockfile-types.d.ts +28 -1
  39. package/dist/live/maintenance.d.ts +20 -4
  40. package/dist/live/maintenance.js +31 -2
  41. package/dist/live/malware.d.ts +20 -0
  42. package/dist/live/malware.js +31 -0
  43. package/dist/live/npm-packument.d.ts +4 -1
  44. package/dist/live/npm-packument.js +5 -2
  45. package/dist/live/offered-release.d.ts +53 -0
  46. package/dist/live/offered-release.js +117 -0
  47. package/dist/live/osv-ranges.d.ts +111 -0
  48. package/dist/live/osv-ranges.js +173 -14
  49. package/dist/live/osv-scanner.d.ts +87 -5
  50. package/dist/live/osv-scanner.js +310 -59
  51. package/dist/live/pinned-advisories.d.ts +2 -2
  52. package/dist/live/pinned-advisories.js +3 -3
  53. package/dist/live/pypi-advice.d.ts +36 -0
  54. package/dist/live/pypi-advice.js +158 -0
  55. package/dist/live/rate-limiter.d.ts +7 -0
  56. package/dist/live/rate-limiter.js +22 -1
  57. package/dist/live/release-metadata.d.ts +5 -0
  58. package/dist/live/resolution.d.ts +3 -0
  59. package/dist/live/resolution.js +6 -3
  60. package/dist/live/scan-coverage.d.ts +26 -0
  61. package/dist/live/scan-coverage.js +70 -0
  62. package/dist/live/semver-utils.d.ts +8 -0
  63. package/dist/live/semver-utils.js +26 -3
  64. package/dist/live/severity-scope.d.ts +26 -1
  65. package/dist/live/severity-scope.js +32 -0
  66. package/dist/live/shell-safe.d.ts +19 -0
  67. package/dist/live/shell-safe.js +36 -0
  68. package/dist/live/types.d.ts +134 -8
  69. package/dist/live/version-compare.d.ts +49 -0
  70. package/dist/live/version-compare.js +131 -0
  71. package/dist/live/withdrawn-releases.d.ts +50 -0
  72. package/dist/live/withdrawn-releases.js +125 -0
  73. package/dist/project-manifests.d.ts +7 -0
  74. package/dist/project-manifests.js +45 -5
  75. package/dist/project-scanner.js +6 -4
  76. package/dist/project-tree.d.ts +56 -0
  77. package/dist/project-tree.js +174 -5
  78. package/dist/schema-registry.d.ts +47 -4
  79. package/dist/schema-registry.js +76 -9
  80. package/dist/server-instructions.js +4 -2
  81. package/dist/setup.d.ts +8 -0
  82. package/dist/setup.js +21 -2
  83. package/dist/tool-dispatch.js +3 -3
  84. package/dist/tools/agent-memory.d.ts +5 -0
  85. package/dist/tools/agent-memory.js +9 -0
  86. package/dist/tools/briefing-task-scope.js +5 -2
  87. package/dist/tools/decision-memory.js +2 -0
  88. package/dist/tools/dependency-check-held.d.ts +44 -0
  89. package/dist/tools/dependency-check-held.js +98 -0
  90. package/dist/tools/dependency-check-signals.d.ts +19 -2
  91. package/dist/tools/dependency-check-signals.js +84 -13
  92. package/dist/tools/dependency-check-versions.d.ts +37 -4
  93. package/dist/tools/dependency-check-versions.js +48 -13
  94. package/dist/tools/dependency-check.d.ts +11 -1
  95. package/dist/tools/dependency-check.js +81 -29
  96. package/dist/tools/dependency-health.js +209 -45
  97. package/dist/tools/get-context.d.ts +3 -0
  98. package/dist/tools/get-context.js +4 -0
  99. package/dist/tools/project-scope.d.ts +8 -0
  100. package/dist/tools/project-scope.js +21 -0
  101. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  102. package/dist/tools/upgrade-impact-features.js +176 -0
  103. package/dist/tools/upgrade-impact-literals.js +26 -4
  104. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  105. package/dist/tools/upgrade-impact-report.js +26 -5
  106. package/dist/tools/upgrade-impact.js +10 -3
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +59 -4
  110. package/dist/tools/upgrade-planner.js +237 -61
  111. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  112. package/dist/tools/vulnerability-scan-concise.js +37 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  114. package/dist/tools/vulnerability-scan-format.js +193 -26
  115. package/dist/tools/vulnerability-scan.d.ts +224 -63
  116. package/dist/tools/vulnerability-scan.js +50 -15
  117. package/dist/tools/what-should-i-know.d.ts +6 -1
  118. package/dist/tools/what-should-i-know.js +72 -15
  119. package/package.json +2 -2
@@ -0,0 +1,131 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * One advisory, many records. OSV publishes the same issue as a GHSA record
4
+ * and as an ecosystem record (PYSEC, RUSTSEC, GO), each naming the other in
5
+ * `aliases`. Counting records counted one issue two or three times:
6
+ * - tokio 1.18.0 showed "6 advisories" for 3 (audit A9);
7
+ * - pygoat's django "96 known vulnerabilities" for 51 (readiness audit M1, 2026-10-16).
8
+ * Every count of advisories goes through here.
9
+ */
10
+ import { isMaintenanceNotice } from "./maintenance.js";
11
+ import { isMalware } from "./malware.js";
12
+ const RANK = { unknown: 0, low: 1, medium: 2, high: 3, critical: 4 };
13
+ /**
14
+ * Group the records of ONE installed copy by shared ids. The grouping is transitive: a record
15
+ * naming ids of two groups joins them.
16
+ */
17
+ export function groupAdvisories(entries) {
18
+ let groups = [];
19
+ for (const v of entries ?? []) {
20
+ const ids = [v.vulnId, ...(v.aliases ?? [])];
21
+ const hit = groups.filter((g) => ids.some((id) => g.ids.has(id)));
22
+ const merged = { ids: new Set(ids), best: v, members: [v] };
23
+ for (const g of hit) {
24
+ for (const id of g.ids)
25
+ merged.ids.add(id);
26
+ merged.members.push(...g.members);
27
+ if ((RANK[g.best.severity] ?? 0) > (RANK[merged.best.severity] ?? 0))
28
+ merged.best = g.best;
29
+ }
30
+ groups = [...groups.filter((g) => !hit.includes(g)), merged];
31
+ }
32
+ return groups;
33
+ }
34
+ /**
35
+ * THE grade of one alias group, shared with the 4DA app (`fourda_depengine::severity`, vectors
36
+ * severity.jsonl sv-001..sv-019): the MOST severe OWN grade among the group's records; only when no
37
+ * record has one, the highest grade among their GHSA aliases that can be read (`aliasGrades`, null =
38
+ * held but ungraded), the smallest id on a tie. An unmaintained-package notice is never graded
39
+ * from an alias.
40
+ */
41
+ export function gradeGroup(records, aliasGrades) {
42
+ let own = null;
43
+ for (const r of records) {
44
+ if (r.own === "unknown")
45
+ continue;
46
+ if (!own || RANK[r.own] > RANK[own.grade])
47
+ own = { id: r.id, grade: r.own };
48
+ }
49
+ let severity = null;
50
+ let inheritedFrom = null;
51
+ if (own) {
52
+ severity = own.grade;
53
+ }
54
+ else {
55
+ const ids = new Set(records.map((r) => r.id));
56
+ const ghsas = [...new Set(records.filter((r) => !r.maintenance).flatMap((r) => r.aliases.filter((a) => /^GHSA-/i.test(a) && !ids.has(a))))].sort();
57
+ let best = null;
58
+ for (const id of ghsas) {
59
+ const g = aliasGrades.get(id);
60
+ if (!g || g === "unknown")
61
+ continue;
62
+ if (!best || RANK[g] > RANK[best.grade])
63
+ best = { id, grade: g };
64
+ }
65
+ if (best) {
66
+ severity = best.grade;
67
+ inheritedFrom = best.id;
68
+ }
69
+ }
70
+ // A malicious-package record (malware.ts) is critical (the 4DA lead, 2026-10-18; sv-010 read high).
71
+ const malicious = records.some((r) => isMalware(r.id, r.aliases, r.cweIds ?? [], r.summary ?? ""));
72
+ const urgency = malicious
73
+ ? "critical"
74
+ : severity === "critical" || severity === "high" || severity === "medium"
75
+ ? severity
76
+ : severity === "low"
77
+ ? "watch"
78
+ : records.every((r) => r.maintenance)
79
+ ? "watch"
80
+ : "medium";
81
+ return { severity, inheritedFrom, ownFrom: own?.id ?? null, urgency };
82
+ }
83
+ /**
84
+ * One severity per advisory, in every tool (the rule shared with the 4DA app, gradeGroup): within
85
+ * one alias group of one installed copy, every record carries the group's grade. vulnerability_scan
86
+ * kept the "richest" record's grade while dependency_check, dependency_health and upgrade_planner
87
+ * kept the most severe, so CVE-2026-4277 (GHSA-pwjp-ccjc-ghwg rated LOW, PYSEC-2026-52 CVSS 9.8) was
88
+ * low in one tool and critical in the others (final release audit of 6.2.5, 2026-10-18). A record
89
+ * that does not carry the grade itself names where it came from (`severityFrom`: the group record,
90
+ * or the GHSA alias of an inherited grade). A grade a record took from its alias
91
+ * (osv-scanner.ts rateFromAliases) never overrides an own grade in its group.
92
+ */
93
+ export function gradeAliasGroups(entries) {
94
+ const byCopy = new Map();
95
+ for (const v of entries) {
96
+ const key = `${v.ecosystem}\u0000${v.package}\u0000${v.currentVersion}\u0000${v.installDriftOf ?? ""}`;
97
+ byCopy.set(key, [...(byCopy.get(key) ?? []), v]);
98
+ }
99
+ const regraded = new Map();
100
+ for (const copy of byCopy.values()) {
101
+ for (const group of groupAdvisories(copy)) {
102
+ if (group.members.length < 2)
103
+ continue;
104
+ const inherited = (m) => Boolean(m.severityFrom) && m.severityFrom !== m.vulnId;
105
+ const aliasGrades = new Map();
106
+ for (const m of group.members)
107
+ if (inherited(m))
108
+ aliasGrades.set(m.severityFrom, m.severity);
109
+ const grade = gradeGroup(group.members.map((m) => ({ id: m.vulnId, own: inherited(m) ? "unknown" : m.severity, aliases: [...m.aliases, ...(inherited(m) ? [m.severityFrom] : [])], maintenance: isMaintenanceNotice(m) })), aliasGrades);
110
+ const severity = grade.severity ?? "unknown";
111
+ const from = grade.inheritedFrom ?? grade.ownFrom;
112
+ for (const m of group.members) {
113
+ if (isMaintenanceNotice(m) && grade.inheritedFrom)
114
+ continue;
115
+ if (m.severity === severity && (!inherited(m) || m.severityFrom === from))
116
+ continue;
117
+ const next = { ...m, severity };
118
+ if (from && from !== m.vulnId)
119
+ next.severityFrom = from;
120
+ else
121
+ delete next.severityFrom;
122
+ regraded.set(m, next);
123
+ }
124
+ }
125
+ }
126
+ return regraded.size === 0 ? [...entries] : entries.map((e) => regraded.get(e) ?? e);
127
+ }
128
+ /** Distinct advisories among the records of one installed copy. */
129
+ export function distinctAdvisoryCount(entries) {
130
+ return groupAdvisories(entries).length;
131
+ }
@@ -37,6 +37,14 @@ export function closeServerCacheDb() {
37
37
  serverCache.close();
38
38
  serverCache = null;
39
39
  }
40
+ /**
41
+ * A row fetched "in the future" was written under a clock that was ahead, or the clock has
42
+ * since gone back (an RTC reset). Its expires_at came from that clock too, so it could keep a
43
+ * year-old "no advisories" answer live: scan 0 vulnerable, dependency_check proceed,
44
+ * what_should_i_know safe_to_delegate (audit 6.2.5 B-3). Such a row is never an answer, fresh or
45
+ * stale, and is purged at start. Five minutes of skew between processes is tolerated.
46
+ */
47
+ const SANE_FETCHED_AT = "fetched_at <= datetime('now', '+5 minutes')";
40
48
  /**
41
49
  * Registry, OSV and changelog responses with a TTL.
42
50
  *
@@ -82,7 +90,7 @@ export class LiveCache {
82
90
  const hit = this.memory.get(key);
83
91
  return hit && this.live(hit) ? JSON.parse(hit.data) : null;
84
92
  }
85
- const row = this.db.prepare("SELECT data FROM live_cache WHERE cache_key = ? AND expires_at > datetime('now')").get(key);
93
+ const row = this.db.prepare(`SELECT data FROM live_cache WHERE cache_key = ? AND expires_at > datetime('now') AND ${SANE_FETCHED_AT}`).get(key);
86
94
  if (!row)
87
95
  return null;
88
96
  try {
@@ -97,7 +105,7 @@ export class LiveCache {
97
105
  const hit = this.memory.get(key);
98
106
  return hit ? { data: JSON.parse(hit.data), fetchedAt: hit.fetched_at } : null;
99
107
  }
100
- const row = this.db.prepare("SELECT data, fetched_at FROM live_cache WHERE cache_key = ?").get(key);
108
+ const row = this.db.prepare(`SELECT data, fetched_at FROM live_cache WHERE cache_key = ? AND ${SANE_FETCHED_AT}`).get(key);
101
109
  if (!row)
102
110
  return null;
103
111
  try {
@@ -150,7 +158,7 @@ export class LiveCache {
150
158
  purged++;
151
159
  return purged;
152
160
  }
153
- const result = this.db.prepare("DELETE FROM live_cache WHERE expires_at <= datetime('now')").run();
161
+ const result = this.db.prepare(`DELETE FROM live_cache WHERE expires_at <= datetime('now') OR NOT (${SANE_FETCHED_AT})`).run();
154
162
  return result.changes;
155
163
  }
156
164
  invalidateSource(source) {
@@ -22,6 +22,12 @@ export declare function activeCratesForHost(dir: string): Set<string> | null;
22
22
  * untrusted input. `CARGO_TARGET_DIR` points at the same scratch directory so
23
23
  * cargo's metadata writes never land in the user's tree either.
24
24
  *
25
+ * The scratch directory lives in the server's per-user data directory, not
26
+ * the system temp directory. On a shared machine `/tmp` is writable by every
27
+ * user, so a `/tmp/.cargo/config.toml` or a pre-created
28
+ * `/tmp/4da-mcp-cargo-resolve/.cargo/config.toml` planted by another account
29
+ * could set `build.rustc-wrapper` and run code as this user (audit 6.2.5 C-2).
30
+ *
25
31
  * Exported for tests.
26
32
  */
27
33
  export declare function cargoTreeInvocation(dir: string): {
@@ -42,8 +42,8 @@
42
42
  */
43
43
  import { execFileSync } from "node:child_process";
44
44
  import * as fs from "node:fs";
45
- import * as os from "node:os";
46
45
  import * as path from "node:path";
46
+ import { serverDataDir } from "../db.js";
47
47
  import { fileSignature } from "./file-signature.js";
48
48
  /** Rust target triple for the current host, or null for a platform we do not map. */
49
49
  export function hostTriple() {
@@ -133,10 +133,16 @@ function computeActiveCrates(dir) {
133
133
  * untrusted input. `CARGO_TARGET_DIR` points at the same scratch directory so
134
134
  * cargo's metadata writes never land in the user's tree either.
135
135
  *
136
+ * The scratch directory lives in the server's per-user data directory, not
137
+ * the system temp directory. On a shared machine `/tmp` is writable by every
138
+ * user, so a `/tmp/.cargo/config.toml` or a pre-created
139
+ * `/tmp/4da-mcp-cargo-resolve/.cargo/config.toml` planted by another account
140
+ * could set `build.rustc-wrapper` and run code as this user (audit 6.2.5 C-2).
141
+ *
136
142
  * Exported for tests.
137
143
  */
138
144
  export function cargoTreeInvocation(dir) {
139
- const scratch = path.join(os.tmpdir(), "4da-mcp-cargo-resolve");
145
+ const scratch = path.join(serverDataDir(), "cargo-resolve");
140
146
  return {
141
147
  args: [
142
148
  "tree",
@@ -107,7 +107,36 @@ export function parseVersionHeading(raw) {
107
107
  return null;
108
108
  return { version, date: extractDate(rest) };
109
109
  }
110
- const ATX = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
110
+ /**
111
+ * An ATX heading `## text ##` as [level, text], parsed by hand in linear time.
112
+ * The regex it replaces (`/^(#{1,6})\s+(.*?)\s*#*\s*$/`) was cubic on interior
113
+ * whitespace: a 1,000-space heading in a package's CHANGELOG took over 20 s on
114
+ * the event loop (audit 2026-10-12, B-10). Same result otherwise ("C#" reads "C").
115
+ */
116
+ function parseAtx(line) {
117
+ const open = /^#{1,6}/.exec(line);
118
+ if (!open)
119
+ return null;
120
+ const level = open[0].length;
121
+ const after = line.slice(level);
122
+ const body = after.trimStart();
123
+ if (body.length === after.length)
124
+ return null; // "#tag" or "#######": not a heading
125
+ let end = body.length;
126
+ while (end > 0 && /\s/.test(body[end - 1]))
127
+ end--;
128
+ while (end > 0 && body[end - 1] === "#")
129
+ end--;
130
+ while (end > 0 && /\s/.test(body[end - 1]))
131
+ end--;
132
+ return [level, body.slice(0, end)];
133
+ }
134
+ /**
135
+ * Lines longer than this are cut before the line-shape regexes run, so every
136
+ * one of them works on bounded input and a hostile line costs milliseconds.
137
+ * No real heading, label or entry comes near it.
138
+ */
139
+ const MAX_LINE = 4096;
111
140
  /**
112
141
  * A setext / reStructuredText underline. `=` and `-` at any length (Markdown);
113
142
  * the other RST adornments need three, so a stray `**` is not a heading rule.
@@ -279,7 +308,10 @@ function adornmentLevel(ch) {
279
308
  }
280
309
  /** Parse a changelog into version sections, in document order (usually newest first). */
281
310
  export function parseChangelog(text) {
282
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
311
+ const lines = text
312
+ .replace(/\r\n?/g, "\n")
313
+ .split("\n")
314
+ .map((l) => (l.length > MAX_LINE ? l.slice(0, MAX_LINE) : l));
283
315
  const state = {
284
316
  sections: [],
285
317
  current: null,
@@ -318,14 +350,14 @@ export function parseChangelog(text) {
318
350
  }
319
351
  if (LINK_DEF.test(line))
320
352
  continue;
321
- const atx = ATX.exec(line);
353
+ const atx = parseAtx(line);
322
354
  if (atx) {
323
- const heading = parseVersionHeading(atx[2]);
324
- const level = atx[1].length;
355
+ const [level, text] = atx;
356
+ const heading = parseVersionHeading(text);
325
357
  if (heading)
326
358
  openSection(state, heading, level);
327
359
  else
328
- subHeading(state, atx[2], level);
360
+ subHeading(state, text, level);
329
361
  continue;
330
362
  }
331
363
  const next = lines[i + 1];
@@ -10,4 +10,12 @@ import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
10
10
  */
11
11
  export declare function commonPathRoot(dirs: string[]): string | null;
12
12
  export declare function dedupeDependencies(deps: ResolvedDependency[]): ResolvedDependency[];
13
- export declare function emptyVulnResult(projectPath: string, offline: boolean): VulnerabilityScanResult;
13
+ /**
14
+ * A scan with no findings. `unchecked` dependencies (offline: none of them
15
+ * could be asked) are counted in `totalScanned` and reported as unchecked,
16
+ * never as clean.
17
+ */
18
+ export declare function emptyVulnResult(projectPath: string, offline: boolean, unchecked?: {
19
+ count: number;
20
+ reason: string;
21
+ } | null): VulnerabilityScanResult;
@@ -77,12 +77,17 @@ function mergeInstallContext(existing, dep) {
77
77
  existing.installedVersion = dep.installedVersion;
78
78
  }
79
79
  }
80
- export function emptyVulnResult(projectPath, offline) {
80
+ /**
81
+ * A scan with no findings. `unchecked` dependencies (offline: none of them
82
+ * could be asked) are counted in `totalScanned` and reported as unchecked,
83
+ * never as clean.
84
+ */
85
+ export function emptyVulnResult(projectPath, offline, unchecked = null) {
81
86
  return {
82
87
  scannedAt: new Date().toISOString(),
83
88
  projectPath,
84
89
  ecosystemsScanned: [],
85
- totalScanned: 0,
90
+ totalScanned: unchecked?.count ?? 0,
86
91
  totalVulnerable: 0,
87
92
  platformInactiveVulnerable: 0,
88
93
  bySeverity: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 },
@@ -91,5 +96,8 @@ export function emptyVulnResult(projectPath, offline) {
91
96
  scanDurationMs: 0,
92
97
  cached: false,
93
98
  offline,
99
+ unchecked: unchecked?.count ?? 0,
100
+ uncheckedReason: unchecked?.reason ?? null,
101
+ staleAnswers: 0,
94
102
  };
95
103
  }
@@ -26,7 +26,38 @@ import type { LiveCache } from "./cache.js";
26
26
  import type { RateLimiter } from "./rate-limiter.js";
27
27
  import { type AdvisoryRanges, type CleanTarget } from "./osv-ranges.js";
28
28
  import { type LockfileFormat } from "./lockfile-parents.js";
29
+ import { type AdvisoryQuery, type HeldDependency } from "./held-dependencies.js";
29
30
  import type { OsvEcosystem } from "./types.js";
31
+ /** What a registry says about one package's releases, for fix targets and parent advice. */
32
+ export interface PackageReleaseFacts {
33
+ /** Published, installable versions: every release except the `withdrawn` ones. */
34
+ versions: string[];
35
+ /**
36
+ * Releases the registry withdrew, never a fix target (withdrawn-releases.ts, the 4DA app's
37
+ * rule): yanked (crates.io; PyPI: every file), and npm `deprecated` only where a later,
38
+ * non-deprecated stable release exists on the same caret line and `latest` is not deprecated.
39
+ */
40
+ withdrawn: string[];
41
+ /** Per withdrawn release, the registry's words ("4.18.0 is deprecated on npm ("Bad release...")"). */
42
+ withdrawnWhy: Record<string, string>;
43
+ /** The registry's latest release (npm `latest` tag; crates.io the highest non-yanked), or null. */
44
+ latest: string | null;
45
+ /** The package-level deprecation of the newest release (npm), or null. */
46
+ deprecatedLatest: string | null;
47
+ /**
48
+ * npm: releases that carry a `deprecated` message the withdrawal rule does NOT withdraw (a
49
+ * notice on a whole line, such as glob's "Old versions of glob are not supported"), with the
50
+ * message. Still candidates; a tool that offers one says it is deprecated (final release audit
51
+ * of 6.2.5: the plan targeted glob 10.5.0 while dependency_check said review on it).
52
+ */
53
+ lineNotices?: Record<string, string>;
54
+ /**
55
+ * npm: per release, the dependencies it holds (exact pins and narrow ranges, held-dependencies.ts):
56
+ * name -> spec. A release that holds none is absent. Its advisories come with the release
57
+ * (audit 6.2.5 D-4: express 4.20.0 exact-pins qs 6.11.0).
58
+ */
59
+ heldSpecs?: Record<string, Record<string, string>>;
60
+ }
30
61
  export interface CrateDepReq {
31
62
  req: string;
32
63
  optional: boolean;
@@ -34,12 +65,20 @@ export interface CrateDepReq {
34
65
  export interface CratesSparse {
35
66
  /** Versions not yanked. */
36
67
  versions: string[];
68
+ /** Yanked versions. */
69
+ yanked?: string[];
37
70
  /** version -> dependency crate name -> requirements (one per target/kind entry; dev-dependencies left out). */
38
71
  reqs: Record<string, Record<string, CrateDepReq[]>>;
39
72
  }
73
+ /** Per published release of a parent: its requirements on one package, and whether it declares it only as a peer. */
74
+ export type RequirementHistory = Record<string, {
75
+ reqs: string[];
76
+ peer: boolean;
77
+ }>;
40
78
  export declare class FixPathSources {
41
79
  private cache;
42
80
  private rateLimiter;
81
+ private npmDocs;
43
82
  constructor(cache: LiveCache, rateLimiter: RateLimiter);
44
83
  /** Every non-withdrawn OSV advisory for a package, as ranges; null when OSV could not be read. */
45
84
  packageAdvisories(ecosystem: OsvEcosystem, name: string): Promise<AdvisoryRanges[] | null>;
@@ -51,15 +90,138 @@ export declare class FixPathSources {
51
90
  * records none). Null when unknown; [] when the parent declares none.
52
91
  */
53
92
  parentRequirements(ecosystem: OsvEcosystem, parent: string, parentVersion: string, dep: string): Promise<string[] | null>;
54
- private npmVersions;
93
+ /**
94
+ * What every published release of `parent` requires of `dep`, from the
95
+ * registry: version -> requirements (`reqs: []` when that release does not
96
+ * depend on it). npm reads dependencies and optionalDependencies of the
97
+ * abbreviated document (as `parentRequirements`), and, when neither holds
98
+ * it, peerDependencies, flagged `peer`: npm 7+ installs a missing peer, so
99
+ * moving a package into peerDependencies is not dropping it (readiness
100
+ * review 2026-10-17, finding 10). crates.io: the sparse index. Null when
101
+ * unknown or unsupported. Used to find the smallest parent release that
102
+ * admits a clean version (audit H1).
103
+ */
104
+ requirementHistory(ecosystem: OsvEcosystem, parent: string, dep: string): Promise<RequirementHistory | null>;
105
+ /**
106
+ * One npm abbreviated document per package, shared by the release facts and the requirement
107
+ * history of a plan (a parent's document was downloaded twice: NodeGoat's mongodb, ~600
108
+ * releases). Kept in memory for a short while, at most a few at a time; a failure is not kept.
109
+ */
110
+ private npmDocument;
111
+ /**
112
+ * Release facts for a parent or a fix target: what is installable, what is
113
+ * deprecated or yanked, and the latest release. Null when the registry
114
+ * could not be read or is not supported.
115
+ */
116
+ releaseFacts(ecosystem: OsvEcosystem, name: string): Promise<PackageReleaseFacts | null>;
117
+ /**
118
+ * npm's abbreviated document: versions without READMEs, with each
119
+ * version's `deprecated` message. A withdrawn release is never offered as a
120
+ * fix target: lodash 4.18.0 ("Bad release. Please use lodash@4.17.21
121
+ * instead.") was recommended by vulnerability_scan (audit B-8, 2026-10-12).
122
+ * Withdrawn means the app's rule (withdrawn-releases.ts): a deprecated
123
+ * release with a later clean one on its caret line. A whole-major notice
124
+ * (glob, tar 6.x) withdraws nothing, or glob 10.4.5 is sent to 12.0.0.
125
+ */
126
+ private npmFacts;
55
127
  /** A crate's sparse-index record: versions and per-version dependency requirements. */
56
128
  cratesSparse(name: string): Promise<CratesSparse | null>;
57
- private pypiVersions;
129
+ /**
130
+ * PyPI's JSON API: every release with files; one whose every file is yanked is withdrawn
131
+ * (PEP 592, the app's `parse_pypi`). A release with no files cannot be installed and is not listed.
132
+ */
133
+ private pypiFacts;
58
134
  private goVersions;
59
135
  }
60
- export type FixTargetResult = CleanTarget;
61
- /** The minimal clean target for one installed copy, or null when the advisories could not be read. */
62
- export declare function cleanFixTarget(sources: FixPathSources, ecosystem: OsvEcosystem, name: string, perAdvisoryFixes: string[]): Promise<FixTargetResult | null>;
136
+ export type FixTargetResult = CleanTarget & {
137
+ /** No target and a malicious landing: "<pkg> is flagged malicious (<id>): remove or replace <pkg>" (#994 review). */
138
+ landingNote?: string;
139
+ /**
140
+ * True when the package's advisories could not be read (OSV /v1/query
141
+ * failed) but the registry could: `target` is then only the smallest
142
+ * installable release at or above the highest per-advisory fix, which is
143
+ * deprecated, yanked or unpublished (`floorWithdrawn` says which). It is
144
+ * NOT checked against the package's other advisories; callers report the
145
+ * fix as checked against the installed version's advisories only.
146
+ */
147
+ advisoriesUnread?: boolean;
148
+ /**
149
+ * True when the package's registry could not be read: withdrawn (yanked, deprecated) releases
150
+ * were not checked, and the target comes from the advisory data alone, as before FX-H5.
151
+ */
152
+ registryUnread?: boolean;
153
+ /** The registry's words for each `withheld` release and each withdrawn line fix. */
154
+ withheldWhy?: Record<string, string>;
155
+ /**
156
+ * Per-advisory fixes the registry withdrew, keyed by that fix: the release to take instead
157
+ * (`fix: null` when none past it is known) and the withdrawn ones passed (installableFix).
158
+ */
159
+ lineFixes?: Record<string, {
160
+ fix: string | null;
161
+ withheld: string[];
162
+ }>;
163
+ /**
164
+ * npm: the target carries a deprecation notice the withdrawal rule keeps (one on its whole line,
165
+ * no later release on the line free of it: glob 10.5.0, "Old versions of glob are not
166
+ * supported"). It stays the target, the smallest fix without a major upgrade, and every tool
167
+ * says it is deprecated, as dependency_check does with `review` (final release audit 7f).
168
+ */
169
+ targetNotice?: string;
170
+ /** The package's malware advisories (#981 rule a): a release one affects is never the target. */
171
+ malware?: string[];
172
+ /**
173
+ * npm (audit 6.2.5 D-4): the release the package's own advisories pointed at, passed because a
174
+ * dependency it holds (an exact pin, or a narrow range whose newest release is affected) brings
175
+ * an advisory in; `heldPast` names each release passed and why. `target` is then the smallest
176
+ * later release on that line whose own advisories AND held dependencies are clean.
177
+ */
178
+ heldFrom?: string;
179
+ heldPast?: Array<{
180
+ version: string;
181
+ affected: HeldDependency[];
182
+ }>;
183
+ /** The target holds an affected dependency and no later release searched (`searched`) clears it: not a clean fix. */
184
+ heldBlocked?: {
185
+ affected: HeldDependency[];
186
+ searched: string[];
187
+ };
188
+ /** Whether the target's held dependencies are affected could not be checked, and why: not a clean fix either. */
189
+ heldUnchecked?: string;
190
+ };
191
+ /**
192
+ * Whether a checked target may be reported as checked against every advisory (fix_checked
193
+ * `all_advisories`): its own advisories were read, and, for npm, what it installs at a fixed
194
+ * version was judged clean (audit 6.2.5 D-4).
195
+ */
196
+ export declare function targetCheckedAll(clean: FixTargetResult): boolean;
197
+ /** The words for a target's held dependencies (FixTargetResult.heldFrom / heldBlocked / heldUnchecked), or null when there is nothing to say. */
198
+ export declare function heldNote(clean: FixTargetResult): string | null;
199
+ /**
200
+ * The minimal clean target for one installed copy, or null when neither the
201
+ * advisories nor anything that changes the answer could be read.
202
+ *
203
+ * Advisories unreadable, registry readable (audit H5: OSV /v1/query down):
204
+ * a deprecated or yanked release is still never the target. lodash 4.17.20
205
+ * went to 4.18.0 ("Bad release") because the whole check was skipped; the
206
+ * registry facts alone move it to 4.18.1, flagged `advisoriesUnread`.
207
+ *
208
+ * With no per-advisory fix at all (advisories that give only `last_affected`,
209
+ * audit A6), and the installed version known, the registry is asked for the
210
+ * first published release above the installed one that no known advisory
211
+ * affects: a fix candidate the advisories never named, flagged
212
+ * `noAdvisoryFix`, instead of "no fix".
213
+ */
214
+ export declare function cleanFixTarget(sources: FixPathSources, ecosystem: OsvEcosystem, name: string, perAdvisoryFixes: string[], installed?: string | null): Promise<FixTargetResult | null>;
215
+ /** #981 (b): an advisory whose ranges cannot be read leaves the release unknown, never skipped silently. */
216
+ export declare function unreadableText(ids: string[]): string;
217
+ /** The 4DA lead's rule (2026-10-18): no release is offered that brings in an advisory the installed version does not have. */
218
+ export declare function introducesText(ids: string[]): string;
219
+ /** #981 round 3: an installed copy a malicious advisory affects names no release; the advice is to remove or replace it. */
220
+ export declare function malwareText(name: string, ids: string[]): string;
221
+ /** The words for a target that carries a line-wide deprecation notice (FixTargetResult.targetNotice). */
222
+ export declare function lineNoticeText(version: string, message: string): string;
223
+ /** `withheldNote` for a checked target: the withdrawn releases passed, in plain words; null when none. */
224
+ export declare function withheldText(clean: FixTargetResult, ecosystem: string): string | null;
63
225
  /**
64
226
  * Verified targets for many installed copies at once, within a time budget:
65
227
  * the copies that could not be checked in time (or offline) are simply
@@ -70,9 +232,27 @@ export declare function verifyFixTargets(sources: FixPathSources | null, items:
70
232
  ecosystem: OsvEcosystem;
71
233
  name: string;
72
234
  fixes: string[];
73
- }>, budgetMs: number, concurrency?: number): Promise<Map<string, CleanTarget>>;
74
- /** One clause for a target that is not the highest per-advisory fix, or null when it is. */
75
- export declare function targetNote(clean: CleanTarget): string | null;
235
+ installed?: string;
236
+ }>, budgetMs: number, concurrency?: number, options?: {
237
+ heldQuery?: AdvisoryQuery;
238
+ }): Promise<Map<string, FixTargetResult>>;
239
+ /** Said whenever the registry could not be read: withdrawn releases are then not ruled out, never silently. */
240
+ export declare const REGISTRY_UNREAD_NOTE = "the registry could not be read, so whether it yanked or deprecated this release was not checked";
241
+ /**
242
+ * One clause for a target that is not the highest per-advisory fix, or null when it is.
243
+ * Withdrawn releases passed on the way are named in the registry's own words (FX-H5).
244
+ */
245
+ export declare function targetNote(clean: FixTargetResult, ecosystem?: string): string | null;
246
+ /**
247
+ * A finding's own fix as the registry still offers it (the app's `installable_fix`): the
248
+ * advisory's fixed version, or, when the registry withdrew it, the release to take instead
249
+ * (null: none past it is known), with the withdrawn ones and the plain-words note.
250
+ */
251
+ export declare function rowFix(fixed: string | null, clean: FixTargetResult | undefined, ecosystem: string): {
252
+ fixed: string | null;
253
+ withheld: string[];
254
+ note: string | null;
255
+ };
76
256
  export interface RefreshVerdict {
77
257
  /** True when a lockfile refresh reaches a clean version (read, or inferred: see `inferred`). */
78
258
  refreshFixes: boolean;
@@ -86,11 +266,15 @@ export interface RefreshVerdict {
86
266
  format: LockfileFormat;
87
267
  /** The command, run in the lockfile's directory; for yarn 1 and bun, the manual steps (no command moves one copy in place). */
88
268
  command: string;
89
- /** Parent requirements that do NOT admit a clean version (why it is waiting on upstream). */
269
+ /**
270
+ * Parent requirements that do NOT admit a clean version (why it is waiting on upstream).
271
+ * `bundled`: the copy ships inside that parent's tarball, so neither a refresh nor an override reaches it (audit E8).
272
+ */
90
273
  blocking: Array<{
91
274
  parent: string;
92
275
  parentVersion: string | null;
93
276
  requirement: string;
277
+ bundled?: boolean;
94
278
  }>;
95
279
  /** The version a refresh resolves to, when the registry's version list was read. */
96
280
  resolvesTo: string | null;
@@ -108,6 +292,70 @@ export interface RefreshVerdict {
108
292
  * pnpm 8.15 to 10.34 also moved a root project's transitive copy without it.
109
293
  */
110
294
  export declare function refreshCommand(format: LockfileFormat, name: string, installed: string, target: string, workspaceRoot?: boolean): string | null;
295
+ /**
296
+ * Whether an override (npm `overrides`, pnpm `pnpm.overrides`, yarn `resolutions`) can reach the copy
297
+ * these blocking parents hold: never when one of them ships it bundled (rule vectors rv-066..rv-068).
298
+ */
299
+ export declare function overrideReaches(blocking: Array<{
300
+ bundled?: boolean;
301
+ }>): boolean;
302
+ /**
303
+ * How a DIRECT dependency moves to its fix target, worded for the tool and the manifest that
304
+ * declares it: a port of the 4DA app's `osv/direct_upgrade.rs` (rule vectors fc-028..fc-053),
305
+ * read from the declaring project's own manifest. A command is named only where it was run
306
+ * against the tool (2026-10-15/16: npm 10.9.2, pnpm 10.34.6, yarn 1.22.22, yarn 4.9.1, cargo
307
+ * 1.95, go 1.24) and moved the dependency while keeping it as declared. Plain `npm install x@v`
308
+ * wrote `^v` over an exact pin and over `~` (readiness review 2026-10-17, finding 4).
309
+ *
310
+ * Null, and the plan keeps its plain "bump it where it is declared", for anything this cannot
311
+ * read with certainty: a dependency declared in two sections; a declared spec that does not
312
+ * admit the installed copy; `>=`, `||`, x-range, tag, git, `workspace:` or alias specs; a git,
313
+ * path or alternate-registry crate; a yarn workspace member (`yarn up` is project-wide); a Go
314
+ * `replace`; hashed or compiled (pip-compile, uv) requirements; bun or an unknown tool.
315
+ */
316
+ export type DirectFix = {
317
+ command: string;
318
+ refresh?: false;
319
+ }
320
+ /**
321
+ * The declaration ALREADY admits the target (navcal: sharp, next and vite under `^`): the minimal
322
+ * advice is the tool's in-range lockfile refresh, which leaves the manifest untouched, not a bump
323
+ * of the declaration (the 4DA app's DirectFix::Refresh). Each was run on a project declaring
324
+ * `ms ^2.1.1` locked at 2.1.1 (2026-10-18): it moved the lock to 2.1.3 and left package.json
325
+ * unchanged; plain `pnpm update ms` rewrote `^2.1.1` to `^2.1.3`, hence `--no-save`. Cargo:
326
+ * `itoa = "1.0.5"` locked 1.0.11 with Cargo.toml unchanged. Any readable range qualifies,
327
+ * `>=` and `||` included: no operator needs carrying.
328
+ */
329
+ | {
330
+ command: string;
331
+ refresh: true;
332
+ } | {
333
+ instruction: string;
334
+ };
335
+ type NpmManager = "npm" | "pnpm" | "yarn1" | "yarn-berry";
336
+ /** The spec `name` is declared with, when exactly one of dependencies / devDependencies / optionalDependencies holds it. */
337
+ export declare function npmDeclared(packageJson: string, name: string): string | null;
338
+ /** The command that moves a declared npm-family dependency, keeping its operator (quoted: `^` is cmd.exe's escape). */
339
+ export declare function npmDirectCommand(manager: NpmManager, declared: string, name: string, target: string): string | null;
340
+ interface CargoDecl {
341
+ kind: "normal" | "dev" | "build";
342
+ target: string | null;
343
+ key: string;
344
+ req: string | null;
345
+ workspace: boolean;
346
+ otherSource: boolean;
347
+ }
348
+ /** Every declaration of `crate` in a Cargo.toml (by crate name, so a renamed dependency is found through `package = ...`). */
349
+ export declare function cargoDeclarations(manifest: string, crate: string): CargoDecl[];
350
+ /** `go get <module>@v<target>` when go.mod requires the module directly at `installed` and no `replace` points it elsewhere. */
351
+ export declare function goDirectCommand(goMod: string, module: string, installed: string, target: string): string | null;
352
+ /** The edit of the requirements line that pins the package with `==` at `installed` (markers, extras kept). */
353
+ export declare function pypiDirectInstruction(requirements: string, file: string, name: string, installed: string, target: string): string | null;
354
+ /**
355
+ * The fix for `name` at `installed`, declared in the project at `projectDir`, read from its own
356
+ * manifest; null when it cannot be named with certainty (see DirectFix).
357
+ */
358
+ export declare function directFix(projectDir: string, ecosystem: string, name: string, installed: string, target: string): DirectFix | null;
111
359
  /** For a lockfile with no in-place refresh command (yarn 1, bun): the steps that move the copy. */
112
360
  export declare function manualRefreshSteps(format: "yarn" | "bun", name: string): string;
113
361
  /** The refresh command for the project whose lockfile is `lockfile`; null when no command is named for it. */
@@ -140,3 +388,4 @@ export declare function refreshVerdict(sources: FixPathSources, ecosystem: OsvEc
140
388
  * at its newest matching version.
141
389
  */
142
390
  export declare function cargoPrecisePrerequisites(sources: FixPathSources, lockContent: string, name: string, installed: string, target: string): Promise<string[] | null>;
391
+ export {};