@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
@@ -86,6 +86,9 @@ const KNOWN_TAG_PREFIX = /^(?:v|version[-_]?|release[-_]?|rel[-_]?)?$/i;
86
86
  */
87
87
  export function versionFromTag(tag, pkg, tagPrefix) {
88
88
  let rest = tag.trim();
89
+ // Real tags are short; the version regex below is superlinear on long ones (B-10).
90
+ if (rest.length > 256)
91
+ return null;
89
92
  if (tagPrefix) {
90
93
  if (!rest.startsWith(`${tagPrefix}/`))
91
94
  return null;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Held dependencies: what an npm release installs whatever the project's lockfile holds.
3
+ *
4
+ * A release's own advisories are not the whole answer to "is this a clean fix". express 4.20.0
5
+ * exact-pins qs 6.11.0 (3 advisories), cookie 0.6.0, body-parser 1.20.3 and path-to-regexp
6
+ * 0.1.10; installing it installs them. vulnerability_scan and dependency_check named it the clean
7
+ * fix for NodeGoat's express 4.16.4, judged on express's own advisories alone, and agents wrote it
8
+ * in 2 of 2 headless runs (audit 6.2.5 D-4). The same holds one step wider: express 4.22.0 to
9
+ * 4.22.2 require qs "~6.14.0" / "~6.15.1", and every qs those ranges admit is affected by
10
+ * GHSA-4mjr-xmp4-gh2g (fixed in 6.16.0), so only 4.22.3 ("~6.16.0") installs clean.
11
+ *
12
+ * So a release HOLDS a dependency when its spec is
13
+ * - an exact pin: a bare version or `=version`, installed as written; or
14
+ * - a narrow range: one capped below the dependency's next major (`~1.2.3`, `^0.1.2`, `1.2.x`,
15
+ * anything with a `<` bound, a hyphen range), whose newest admitted release is the best any
16
+ * install can do (npm takes `latest` when the range admits it, else the highest it admits).
17
+ * A wide range (`^6.14.0`, `>=1`) is not held: a refresh can resolve it to the dependency's newest
18
+ * release on that major, and moving it is upgrade_planner's lever logic.
19
+ *
20
+ * A held dependency whose installed version an advisory affects brings that advisory with the
21
+ * release. The question is asked of OSV through the scanner the tools already use
22
+ * (LiveIntelligence.queryAdvisories: batched, cached per dependency, rate-limited), in one call per
23
+ * check. Fail closed: when a range's releases or OSV's answer cannot be had, the release is
24
+ * `unchecked` with the reason, and no tool calls it clean on that.
25
+ */
26
+ import { type AdvisoryRanges } from "./osv-ranges.js";
27
+ import type { ResolvedDependency, VulnerabilityEntry, VulnerabilityScanResult } from "./types.js";
28
+ /** The registry facts this module reads (fix-paths.ts PackageReleaseFacts; a type only, no import cycle). */
29
+ export interface HeldFacts {
30
+ versions: string[];
31
+ withdrawn: string[];
32
+ latest: string | null;
33
+ /** Per release: its held specs (dependency name -> spec). A release with none is absent. */
34
+ heldSpecs?: Record<string, Record<string, string>>;
35
+ }
36
+ export interface HeldSources {
37
+ releaseFacts(ecosystem: "npm", name: string): Promise<HeldFacts | null>;
38
+ }
39
+ /** The advisory lookup the tools share (LiveIntelligence.queryAdvisories). */
40
+ export type AdvisoryQuery = (deps: ResolvedDependency[]) => Promise<VulnerabilityScanResult>;
41
+ export interface HeldDependency {
42
+ name: string;
43
+ /** The release's own spec for it, as published. */
44
+ spec: string;
45
+ /** True for an exact pin; false for a narrow range. */
46
+ exact: boolean;
47
+ /** What an install of the release gets: the pin, or the newest release the range admits. */
48
+ version: string;
49
+ /** The advisories that affect `version` (one id per alias group, maintenance notices left out). */
50
+ ids: string[];
51
+ severity: VulnerabilityEntry["severity"];
52
+ }
53
+ export type HeldCheck = {
54
+ status: "checked";
55
+ affected: HeldDependency[];
56
+ } | {
57
+ status: "unchecked";
58
+ reason: string;
59
+ };
60
+ /** How many releases a fix-target search judges at most (the advisories' target and the next on its line). */
61
+ export declare const HELD_CANDIDATES = 10;
62
+ /** The version an exact npm pin installs ("6.11.0", "=6.11.0", "v6.11.0"); null for anything else. */
63
+ export declare function exactPinVersion(spec: unknown): string | null;
64
+ /** Whether an npm range is capped below its dependency's next major (see the module comment). */
65
+ export declare function isNarrowRange(spec: string): boolean;
66
+ /** A release's held specs: exact pins and narrow ranges, dependencies and optionalDependencies (npm installs both). */
67
+ export declare function heldSpecsOf(dependencies?: Record<string, unknown>, optionalDependencies?: Record<string, unknown>): Record<string, string>;
68
+ /**
69
+ * For each release (`key` -> its held specs), which held dependencies an advisory affects: one OSV
70
+ * call for all of them, the narrow ranges resolved from each dependency's registry facts first.
71
+ * A release whose held dependencies could not all be resolved and judged is `unchecked`, unless
72
+ * one that could be is already affected (a concrete finding stands).
73
+ */
74
+ export declare function heldAdvisories(sources: HeldSources, query: AdvisoryQuery, releases: Array<{
75
+ key: string;
76
+ specs: Record<string, string>;
77
+ }>, deadline: number): Promise<Map<string, HeldCheck>>;
78
+ /**
79
+ * "4.20.0 exact-pins qs 6.11.0 (GHSA-a, GHSA-b) and cookie 0.6.0 (GHSA-c)", and for a narrow range
80
+ * "4.22.0 requires qs "~6.14.0", whose newest admitted release 6.14.2 is affected (GHSA-d)".
81
+ * At most three dependencies are named; the rest are counted.
82
+ */
83
+ export declare function heldText(release: string, affected: HeldDependency[]): string;
84
+ /** The worst severity among held dependencies' advisories. */
85
+ export declare function heldSeverity(affected: HeldDependency[]): VulnerabilityEntry["severity"];
86
+ /** Same compatibility line in npm's caret sense: same major, or same 0.minor below 1.0. */
87
+ export declare function sameNpmLine(a: string, b: string): boolean;
88
+ /**
89
+ * The releases a fix-target search judges, smallest first: `target`, then the later stable,
90
+ * installable releases on its line that the package's own advisories leave clean (the one
91
+ * predicate every tool uses, osv-ranges cleanReleases), HELD_CANDIDATES in all at most.
92
+ */
93
+ export declare function heldCandidates(target: string, facts: HeldFacts, advisories: readonly AdvisoryRanges[], installed?: string | null): string[];
@@ -0,0 +1,212 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Held dependencies: what an npm release installs whatever the project's lockfile holds.
4
+ *
5
+ * A release's own advisories are not the whole answer to "is this a clean fix". express 4.20.0
6
+ * exact-pins qs 6.11.0 (3 advisories), cookie 0.6.0, body-parser 1.20.3 and path-to-regexp
7
+ * 0.1.10; installing it installs them. vulnerability_scan and dependency_check named it the clean
8
+ * fix for NodeGoat's express 4.16.4, judged on express's own advisories alone, and agents wrote it
9
+ * in 2 of 2 headless runs (audit 6.2.5 D-4). The same holds one step wider: express 4.22.0 to
10
+ * 4.22.2 require qs "~6.14.0" / "~6.15.1", and every qs those ranges admit is affected by
11
+ * GHSA-4mjr-xmp4-gh2g (fixed in 6.16.0), so only 4.22.3 ("~6.16.0") installs clean.
12
+ *
13
+ * So a release HOLDS a dependency when its spec is
14
+ * - an exact pin: a bare version or `=version`, installed as written; or
15
+ * - a narrow range: one capped below the dependency's next major (`~1.2.3`, `^0.1.2`, `1.2.x`,
16
+ * anything with a `<` bound, a hyphen range), whose newest admitted release is the best any
17
+ * install can do (npm takes `latest` when the range admits it, else the highest it admits).
18
+ * A wide range (`^6.14.0`, `>=1`) is not held: a refresh can resolve it to the dependency's newest
19
+ * release on that major, and moving it is upgrade_planner's lever logic.
20
+ *
21
+ * A held dependency whose installed version an advisory affects brings that advisory with the
22
+ * release. The question is asked of OSV through the scanner the tools already use
23
+ * (LiveIntelligence.queryAdvisories: batched, cached per dependency, rate-limited), in one call per
24
+ * check. Fail closed: when a range's releases or OSV's answer cannot be had, the release is
25
+ * `unchecked` with the reason, and no tool calls it clean on that.
26
+ */
27
+ import { cleanReleases, isPrereleaseVersion } from "./osv-ranges.js";
28
+ import { groupAdvisories } from "./advisory-groups.js";
29
+ import { isMaintenanceNotice } from "./maintenance.js";
30
+ import { npmRangeAdmits } from "./range-satisfies.js";
31
+ import { SEVERITY_RANK } from "./severity-scope.js";
32
+ import { compareVersions } from "./version-compare.js";
33
+ /** How many releases a fix-target search judges at most (the advisories' target and the next on its line). */
34
+ export const HELD_CANDIDATES = 10;
35
+ /** Registry reads in flight at once for the narrow ranges' release lists. */
36
+ const FACTS_CONCURRENCY = 8;
37
+ const EXACT = /^=?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)(?:\+[0-9A-Za-z.-]+)?$/;
38
+ /** The version an exact npm pin installs ("6.11.0", "=6.11.0", "v6.11.0"); null for anything else. */
39
+ export function exactPinVersion(spec) {
40
+ if (typeof spec !== "string")
41
+ return null;
42
+ const m = EXACT.exec(spec.trim());
43
+ return m ? m[1] : null;
44
+ }
45
+ /** Whether an npm range is capped below its dependency's next major (see the module comment). */
46
+ export function isNarrowRange(spec) {
47
+ const s = spec.trim();
48
+ if (s === "" || s.includes("||") || exactPinVersion(s) !== null)
49
+ return false;
50
+ // Protocols, paths, aliases and dist-tags are not registry ranges this reads.
51
+ if (/^[a-z][a-z+]*:/i.test(s) || s.includes("/") || /^(?!v\d)[a-z]/i.test(s))
52
+ return false;
53
+ if (s.startsWith("~"))
54
+ return true;
55
+ if (/^\^?\s*v?0\./.test(s))
56
+ return true;
57
+ if (/^v?\d+\.\d+(?:\.[xX*])?$/.test(s))
58
+ return true;
59
+ if (s.includes("<"))
60
+ return true;
61
+ return /^\S+\s+-\s+\S+$/.test(s);
62
+ }
63
+ /** A release's held specs: exact pins and narrow ranges, dependencies and optionalDependencies (npm installs both). */
64
+ export function heldSpecsOf(dependencies, optionalDependencies) {
65
+ const out = {};
66
+ // An optional dependency's spec wins over the same name in dependencies, as npm reads it.
67
+ for (const [name, spec] of Object.entries({ ...dependencies, ...optionalDependencies })) {
68
+ if (typeof spec !== "string")
69
+ continue;
70
+ if (exactPinVersion(spec) !== null || isNarrowRange(spec))
71
+ out[name] = spec.trim();
72
+ }
73
+ return out;
74
+ }
75
+ /** The release an install of `spec` gets, from the dependency's registry facts; null when it cannot be said. */
76
+ function resolveSpec(spec, facts) {
77
+ const pinned = exactPinVersion(spec);
78
+ if (pinned !== null)
79
+ return pinned;
80
+ if (!facts)
81
+ return null;
82
+ const admits = (v) => npmRangeAdmits(spec, v) === true;
83
+ if (facts.latest && admits(facts.latest))
84
+ return facts.latest;
85
+ const pool = [...facts.versions, ...facts.withdrawn].filter((v) => (spec.includes("-") || !isPrereleaseVersion(v, "npm")) && admits(v));
86
+ return pool.sort((a, b) => compareVersions(a, b, "npm")).pop() ?? null;
87
+ }
88
+ function lookupDep(name, version) {
89
+ return { name, version, ecosystem: "npm", isDev: false, isDirect: false, devScopeKnown: false, target: null, platformActive: true, sourceDirs: [] };
90
+ }
91
+ /** Resolves to `late` once `deadline` passes. */
92
+ async function byDeadline(work, deadline, late) {
93
+ let timer;
94
+ const timeout = new Promise((resolve) => {
95
+ timer = setTimeout(() => resolve(late), Math.max(0, deadline - Date.now()));
96
+ });
97
+ try {
98
+ return await Promise.race([work.catch(() => late), timeout]);
99
+ }
100
+ finally {
101
+ if (timer !== undefined)
102
+ clearTimeout(timer);
103
+ }
104
+ }
105
+ /**
106
+ * For each release (`key` -> its held specs), which held dependencies an advisory affects: one OSV
107
+ * call for all of them, the narrow ranges resolved from each dependency's registry facts first.
108
+ * A release whose held dependencies could not all be resolved and judged is `unchecked`, unless
109
+ * one that could be is already affected (a concrete finding stands).
110
+ */
111
+ export async function heldAdvisories(sources, query, releases, deadline) {
112
+ const out = new Map();
113
+ const ranged = [...new Set(releases.flatMap((r) => Object.entries(r.specs).filter(([, s]) => exactPinVersion(s) === null).map(([n]) => n)))];
114
+ const facts = new Map();
115
+ let next = 0;
116
+ await Promise.all(Array.from({ length: Math.min(FACTS_CONCURRENCY, ranged.length) }, async () => {
117
+ while (next < ranged.length && Date.now() < deadline) {
118
+ const name = ranged[next++];
119
+ facts.set(name, await byDeadline(sources.releaseFacts("npm", name), deadline, null));
120
+ }
121
+ }));
122
+ const resolved = releases.map((r) => ({
123
+ key: r.key,
124
+ deps: Object.entries(r.specs).map(([name, spec]) => ({ name, spec, exact: exactPinVersion(spec) !== null, version: resolveSpec(spec, facts.get(name)) })),
125
+ }));
126
+ const wanted = new Map();
127
+ for (const r of resolved)
128
+ for (const d of r.deps)
129
+ if (d.version)
130
+ wanted.set(`${d.name}@${d.version}`, lookupDep(d.name, d.version));
131
+ let answer = null;
132
+ let failure = null;
133
+ if (wanted.size > 0) {
134
+ answer = await byDeadline(query([...wanted.values()]), deadline, null);
135
+ if (!answer)
136
+ failure = "the OSV.dev lookup did not finish in time";
137
+ else if (answer.offline)
138
+ failure = answer.uncheckedReason ?? "OSV.dev could not be queried";
139
+ else if ((answer.staleAnswers ?? 0) > 0)
140
+ failure = `OSV.dev gave only expired cached answers (${answer.staleReason ?? "the fresh query failed"})`;
141
+ }
142
+ const byCopy = new Map();
143
+ for (const v of failure ? [] : (answer?.vulnerabilities ?? [])) {
144
+ if (isMaintenanceNotice(v))
145
+ continue;
146
+ const k = `${v.package}@${v.currentVersion}`;
147
+ byCopy.set(k, [...(byCopy.get(k) ?? []), v]);
148
+ }
149
+ const hits = new Map();
150
+ for (const [k, entries] of byCopy) {
151
+ const groups = groupAdvisories(entries);
152
+ const worst = groups.map((g) => g.best.severity).reduce((a, b) => ((SEVERITY_RANK[b] ?? 0) > (SEVERITY_RANK[a] ?? 0) ? b : a), "unknown");
153
+ hits.set(k, { ids: groups.map((g) => g.best.vulnId).sort(), severity: worst });
154
+ }
155
+ for (const r of resolved) {
156
+ const affected = r.deps.flatMap((d) => {
157
+ const hit = d.version ? hits.get(`${d.name}@${d.version}`) : undefined;
158
+ return hit ? [{ name: d.name, spec: d.spec, exact: d.exact, version: d.version, ids: hit.ids, severity: hit.severity }] : [];
159
+ });
160
+ const unread = r.deps.filter((d) => !d.version).map((d) => `${d.name} "${d.spec}"`);
161
+ if (affected.length > 0)
162
+ out.set(r.key, { status: "checked", affected });
163
+ else if (unread.length > 0)
164
+ out.set(r.key, { status: "unchecked", reason: `the releases ${unread.slice(0, 3).join(", ")}${unread.length > 3 ? ` and ${unread.length - 3} more` : ""} admit could not be read from the npm registry` });
165
+ else if (failure && r.deps.length > 0)
166
+ out.set(r.key, { status: "unchecked", reason: failure });
167
+ else
168
+ out.set(r.key, { status: "checked", affected: [] });
169
+ }
170
+ return out;
171
+ }
172
+ /**
173
+ * "4.20.0 exact-pins qs 6.11.0 (GHSA-a, GHSA-b) and cookie 0.6.0 (GHSA-c)", and for a narrow range
174
+ * "4.22.0 requires qs "~6.14.0", whose newest admitted release 6.14.2 is affected (GHSA-d)".
175
+ * At most three dependencies are named; the rest are counted.
176
+ */
177
+ export function heldText(release, affected) {
178
+ const shown = affected.slice(0, 3);
179
+ const and = (parts) => (parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0]);
180
+ const exact = shown.filter((d) => d.exact).map((d) => `${d.name} ${d.version} (${d.ids.join(", ")})`);
181
+ const ranged = shown
182
+ .filter((d) => !d.exact)
183
+ .map((d) => `${d.name} "${d.spec}", whose newest admitted release ${d.version} is affected (${d.ids.join(", ")})`);
184
+ const clauses = [...(exact.length > 0 ? [`exact-pins ${and(exact)}`] : []), ...(ranged.length > 0 ? [`requires ${and(ranged)}`] : [])];
185
+ const rest = affected.length - shown.length;
186
+ const more = rest > 0 ? `, and ${rest} more affected dependenc${rest === 1 ? "y" : "ies"}` : "";
187
+ return `${release} ${clauses.join("; it also ")}${more}`;
188
+ }
189
+ /** The worst severity among held dependencies' advisories. */
190
+ export function heldSeverity(affected) {
191
+ return affected.reduce((worst, d) => ((SEVERITY_RANK[d.severity] ?? 0) > (SEVERITY_RANK[worst] ?? 0) ? d.severity : worst), "unknown");
192
+ }
193
+ /** Same compatibility line in npm's caret sense: same major, or same 0.minor below 1.0. */
194
+ export function sameNpmLine(a, b) {
195
+ const pa = /^v?(\d+)\.(\d+)\./.exec(a);
196
+ const pb = /^v?(\d+)\.(\d+)\./.exec(b);
197
+ if (!pa || !pb)
198
+ return false;
199
+ return pa[1] === pb[1] && (pa[1] !== "0" || pa[2] === pb[2]);
200
+ }
201
+ /**
202
+ * The releases a fix-target search judges, smallest first: `target`, then the later stable,
203
+ * installable releases on its line that the package's own advisories leave clean (the one
204
+ * predicate every tool uses, osv-ranges cleanReleases), HELD_CANDIDATES in all at most.
205
+ */
206
+ export function heldCandidates(target, facts, advisories, installed) {
207
+ const view = cleanReleases(advisories, "npm", facts.versions, installed);
208
+ const later = facts.versions
209
+ .filter((v) => !isPrereleaseVersion(v, "npm") && sameNpmLine(v, target) && compareVersions(v, target, "npm") > 0 && view.clean(v))
210
+ .sort((a, b) => compareVersions(a, b, "npm"));
211
+ return [target, ...later].slice(0, HELD_CANDIDATES);
212
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fix targets judged by what they hold, not only by their own advisories (audit 6.2.5 D-4).
3
+ *
4
+ * cleanFixTarget (fix-paths.ts) picks the smallest release that no advisory of the package
5
+ * affects. For npm that is not yet a clean fix: express 4.20.0 is clear of express's advisories
6
+ * and exact-pins qs 6.11.0, cookie 0.6.0, body-parser 1.20.3 and path-to-regexp 0.1.10, all
7
+ * affected; 4.22.0 to 4.22.2 require qs ranges no clean qs is in. NodeGoat's scan said
8
+ * "Upgrade express 4.16.4 -> 4.20.0", and agents installed it.
9
+ *
10
+ * Here a checked npm target is judged with its held dependencies (held-dependencies.ts). When
11
+ * one is affected, the next releases on its line that the package's own advisories leave clean are
12
+ * judged too, HELD_CANDIDATES in all and in ONE advisory lookup for every target at once, and the
13
+ * smallest that holds nothing affected becomes the target, with the reason. None clean among
14
+ * them: the target stays, flagged `heldBlocked`; the check could not run (registry, OSV, time):
15
+ * flagged `heldUnchecked`. Either way the target is no longer reported as checked against every
16
+ * advisory (targetCheckedAll). A release that holds nothing needs no lookup at all.
17
+ */
18
+ import { type AdvisoryQuery } from "./held-dependencies.js";
19
+ import { type FixPathSources, type FixTargetResult } from "./fix-paths.js";
20
+ export interface HeldTargetItem {
21
+ key: string;
22
+ name: string;
23
+ installed?: string | null;
24
+ clean: FixTargetResult;
25
+ }
26
+ /**
27
+ * The npm targets of `items`, re-judged by what they hold, within `deadline`. Returns the
28
+ * replacement result per key; keys with nothing held (or not npm, or not checked) are absent.
29
+ */
30
+ export declare function withHeldTargets(sources: FixPathSources, query: AdvisoryQuery, items: HeldTargetItem[], deadline: number): Promise<Map<string, FixTargetResult>>;
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Fix targets judged by what they hold, not only by their own advisories (audit 6.2.5 D-4).
4
+ *
5
+ * cleanFixTarget (fix-paths.ts) picks the smallest release that no advisory of the package
6
+ * affects. For npm that is not yet a clean fix: express 4.20.0 is clear of express's advisories
7
+ * and exact-pins qs 6.11.0, cookie 0.6.0, body-parser 1.20.3 and path-to-regexp 0.1.10, all
8
+ * affected; 4.22.0 to 4.22.2 require qs ranges no clean qs is in. NodeGoat's scan said
9
+ * "Upgrade express 4.16.4 -> 4.20.0", and agents installed it.
10
+ *
11
+ * Here a checked npm target is judged with its held dependencies (held-dependencies.ts). When
12
+ * one is affected, the next releases on its line that the package's own advisories leave clean are
13
+ * judged too, HELD_CANDIDATES in all and in ONE advisory lookup for every target at once, and the
14
+ * smallest that holds nothing affected becomes the target, with the reason. None clean among
15
+ * them: the target stays, flagged `heldBlocked`; the check could not run (registry, OSV, time):
16
+ * flagged `heldUnchecked`. Either way the target is no longer reported as checked against every
17
+ * advisory (targetCheckedAll). A release that holds nothing needs no lookup at all.
18
+ */
19
+ import { heldAdvisories, heldCandidates } from "./held-dependencies.js";
20
+ import { cleanReleases } from "./osv-ranges.js";
21
+ import { lineNoticeText } from "./fix-paths.js";
22
+ /**
23
+ * The npm targets of `items`, re-judged by what they hold, within `deadline`. Returns the
24
+ * replacement result per key; keys with nothing held (or not npm, or not checked) are absent.
25
+ */
26
+ export async function withHeldTargets(sources, query, items, deadline) {
27
+ const out = new Map();
28
+ // Only a target that was checked against the package's own advisories can be moved on from;
29
+ // one that was not is already reported as checked against the installed version's advisories only.
30
+ const todo = items.filter((i) => i.clean.target && !i.clean.advisoriesUnread);
31
+ const plans = [];
32
+ await Promise.all(todo.map(async (item) => {
33
+ const target = item.clean.target;
34
+ const facts = await settle(sources.releaseFacts("npm", item.name), deadline);
35
+ if (!facts) {
36
+ out.set(item.key, { ...item.clean, heldUnchecked: "the npm registry could not be read in time" });
37
+ return;
38
+ }
39
+ if (!facts.heldSpecs) {
40
+ out.set(item.key, { ...item.clean, heldUnchecked: "the npm registry facts carry no dependency specs" });
41
+ return;
42
+ }
43
+ if (!facts.heldSpecs[target])
44
+ return; // holds nothing: what it installs is ranges a refresh moves
45
+ const advisories = await settle(sources.packageAdvisories("npm", item.name), deadline);
46
+ // Without the package's own advisory list no later release can be judged; the target alone still can.
47
+ const candidates = advisories ? heldCandidates(target, facts, advisories, item.installed) : [target];
48
+ plans.push({ item, candidates, facts });
49
+ }));
50
+ if (plans.length === 0)
51
+ return out;
52
+ const releases = plans.flatMap((p) => p.candidates.map((v) => ({ key: `${p.item.name}@${v}`, specs: p.facts.heldSpecs[v] ?? {} })));
53
+ const checks = await heldAdvisories(sources, query, releases, deadline);
54
+ for (const { item, candidates, facts } of plans) {
55
+ const target = item.clean.target;
56
+ const at = (v) => checks.get(`${item.name}@${v}`) ?? { status: "unchecked", reason: "the check did not finish in time" };
57
+ const own = at(target);
58
+ if (own.status === "unchecked") {
59
+ out.set(item.key, { ...item.clean, heldUnchecked: own.reason });
60
+ continue;
61
+ }
62
+ if (own.affected.length === 0)
63
+ continue;
64
+ const past = [];
65
+ let chosen = null;
66
+ for (const v of candidates) {
67
+ const c = at(v);
68
+ if (c.status === "checked" && c.affected.length === 0) {
69
+ chosen = v;
70
+ break;
71
+ }
72
+ // An unchecked candidate is passed silently: the one chosen after it was itself judged clean.
73
+ if (c.status === "checked")
74
+ past.push({ version: v, affected: c.affected });
75
+ }
76
+ if (!chosen) {
77
+ out.set(item.key, { ...item.clean, heldBlocked: { affected: own.affected, searched: candidates } });
78
+ continue;
79
+ }
80
+ const view = cleanReleases((await settle(sources.packageAdvisories("npm", item.name), deadline)) ?? [], "npm", facts.versions, item.installed);
81
+ const notice = facts.lineNotices?.[chosen];
82
+ const { targetNotice: _old, ...rest } = item.clean;
83
+ out.set(item.key, {
84
+ ...rest,
85
+ target: chosen,
86
+ stillAffectedBy: view.remains(chosen),
87
+ heldFrom: target,
88
+ heldPast: past,
89
+ ...(notice ? { targetNotice: lineNoticeText(chosen, notice) } : {}),
90
+ });
91
+ }
92
+ return out;
93
+ }
94
+ /** The value, or null when it fails or `deadline` passes first. */
95
+ async function settle(work, deadline) {
96
+ let timer;
97
+ const late = new Promise((resolve) => {
98
+ timer = setTimeout(() => resolve(null), Math.max(0, deadline - Date.now()));
99
+ });
100
+ try {
101
+ return await Promise.race([work.catch(() => null), late]);
102
+ }
103
+ finally {
104
+ if (timer !== undefined)
105
+ clearTimeout(timer);
106
+ }
107
+ }
@@ -2,12 +2,22 @@
2
2
  const DEFAULT_TIMEOUT_MS = 10_000;
3
3
  export async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
4
4
  const controller = new AbortController();
5
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
5
+ let timeout = setTimeout(() => controller.abort(), timeoutMs);
6
6
  try {
7
- return await fetch(url, { ...options, signal: controller.signal });
7
+ const response = await fetch(url, { ...options, signal: controller.signal });
8
+ // The headers are in; the body is not read yet. Clearing the abort here left json(), text()
9
+ // and arrayBuffer() with no deadline: a server that stalled mid-body hung vulnerability_scan
10
+ // and dependency_check, and every later scan that joined the stuck one (audit 6.2.5 B-6).
11
+ // The abort stays armed for the body, with its own allowance; unref'd, it never holds the
12
+ // process open, and aborting a body already read does nothing.
13
+ clearTimeout(timeout);
14
+ timeout = setTimeout(() => controller.abort(), timeoutMs);
15
+ timeout.unref?.();
16
+ return response;
8
17
  }
9
- finally {
18
+ catch (error) {
10
19
  clearTimeout(timeout);
20
+ throw error;
11
21
  }
12
22
  }
13
23
  export async function fetchJson(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
@@ -13,6 +13,7 @@ import { FixPathSources } from "./fix-paths.js";
13
13
  import type { CheckEcosystem, ReleaseMetadataSource } from "./release-metadata.js";
14
14
  import { type ResolutionGroup } from "./resolution.js";
15
15
  import type { TreeDiscovery } from "../project-tree.js";
16
+ import type { LockfileProblem } from "./lockfile-types.js";
16
17
  import type { InstallDriftRecord, ResolutionProvenance, ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
17
18
  export type { VulnerabilityScanResult, VulnerabilityEntry, LiveHeadline, LiveIntelligenceStatus, RegistryPackageInfo, DependencyHealthResult, InstallDriftRecord, ResolutionProvenance, } from "./types.js";
18
19
  export declare class LiveIntelligence {
@@ -116,6 +117,8 @@ export declare class LiveIntelligence {
116
117
  manifest: string;
117
118
  ranges: number;
118
119
  }>;
120
+ /** Lockfiles present in a resolved directory that could not be read, each with the reason. */
121
+ getLockfileProblems(): LockfileProblem[];
119
122
  /** What the project-tree walk left out (lockfiles past its bounds); null when the set did not come from a walk. */
120
123
  getDiscovery(): TreeDiscovery | null;
121
124
  /**
@@ -160,10 +163,10 @@ export declare class LiveIntelligence {
160
163
  /**
161
164
  * Run vulnerability scan (returns cached if fresh, fetches otherwise).
162
165
  *
163
- * Without `includeDev`, only DIRECT devDependencies are left out (the
164
- * documented contract). A transitive stays in whatever its scope: an
165
- * unknown scope must not be hidden, and a known dev-only one is graded
166
- * down by the shared severity rule, not dropped.
166
+ * Without `includeDev`, every dev-only copy is left out (severity-scope.ts
167
+ * isDevOnlyCopy: a direct devDependency, or a transitive whose every path
168
+ * is a dev one), the population dependency_health and upgrade_planner leave
169
+ * out too. A copy of unknown scope stays: it must not be hidden.
167
170
  */
168
171
  scanVulnerabilities(projectPath: string, options?: {
169
172
  includeDev?: boolean;
@@ -23,9 +23,11 @@ import { GoReleasesReader } from "./go-releases.js";
23
23
  import { FixPathSources } from "./fix-paths.js";
24
24
  import { fetchRegistryHealthFor } from "./registry-health.js";
25
25
  import { commonPathRoot, dedupeDependencies, emptyVulnResult } from "./dependency-set.js";
26
+ import { LOOKUPS_DISABLED } from "./scan-coverage.js";
26
27
  import { groupIsStale, resolveGroup } from "./resolution.js";
27
28
  import { mapEcosystem } from "./version-resolver.js";
28
29
  import { applyInstanceDevScope } from "./dev-scope.js";
30
+ import { isDevOnlyCopy } from "./severity-scope.js";
29
31
  /** Minimum gap between vulnerability warmup attempts after an offline result. */
30
32
  const WARMUP_RETRY_MS = 60_000;
31
33
  export class LiveIntelligence {
@@ -160,6 +162,11 @@ export class LiveIntelligence {
160
162
  getUnresolvedManifests() {
161
163
  return this.groupResults.flatMap((r) => (r.unresolved ? [{ dir: r.group.dir, ...r.unresolved }] : []));
162
164
  }
165
+ /** Lockfiles present in a resolved directory that could not be read, each with the reason. */
166
+ getLockfileProblems() {
167
+ const seen = new Set();
168
+ return this.groupResults.flatMap((r) => r.problems ?? []).filter((p) => !seen.has(p.file) && (seen.add(p.file), true));
169
+ }
163
170
  /** What the project-tree walk left out (lockfiles past its bounds); null when the set did not come from a walk. */
164
171
  getDiscovery() {
165
172
  return this.discovery;
@@ -250,21 +257,19 @@ export class LiveIntelligence {
250
257
  /**
251
258
  * Run vulnerability scan (returns cached if fresh, fetches otherwise).
252
259
  *
253
- * Without `includeDev`, only DIRECT devDependencies are left out (the
254
- * documented contract). A transitive stays in whatever its scope: an
255
- * unknown scope must not be hidden, and a known dev-only one is graded
256
- * down by the shared severity rule, not dropped.
260
+ * Without `includeDev`, every dev-only copy is left out (severity-scope.ts
261
+ * isDevOnlyCopy: a direct devDependency, or a transitive whose every path
262
+ * is a dev one), the population dependency_health and upgrade_planner leave
263
+ * out too. A copy of unknown scope stays: it must not be hidden.
257
264
  */
258
265
  async scanVulnerabilities(projectPath, options) {
259
266
  if (!this.enabled) {
260
- return emptyVulnResult(projectPath, true);
267
+ return emptyVulnResult(projectPath, true, { count: this.auditDeps.length, reason: LOOKUPS_DISABLED });
261
268
  }
262
- const scoped = options?.includeDev
263
- ? this.auditDeps
264
- : this.auditDeps.filter((d) => !(d.isDirect && d.isDev));
265
269
  // Read at scan time, not init time: the app rewrites its inventory on
266
270
  // every rescan, and the freshest determination is the one to grade by.
267
- const deps = applyInstanceDevScope(this.db, scoped);
271
+ const scoped = applyInstanceDevScope(this.db, this.auditDeps);
272
+ const deps = options?.includeDev ? scoped : scoped.filter((d) => !isDevOnlyCopy(d));
268
273
  if (deps.length === 0) {
269
274
  return emptyVulnResult(projectPath, false);
270
275
  }
@@ -297,12 +302,12 @@ export class LiveIntelligence {
297
302
  }
298
303
  return result;
299
304
  }
300
- catch {
301
- // Network failure — return last known or empty
302
- if (generation === this.generation && this.lastVulnScan) {
303
- return { ...this.lastVulnScan, offline: true, cached: true };
304
- }
305
- return emptyVulnResult(projectPath, true);
305
+ catch (error) {
306
+ // The scanner reports unreachable dependencies itself; reaching here is
307
+ // an unexpected failure. Nothing was checked, and the answer says so
308
+ // (it used to hand back the last scan or an empty one, read as clean).
309
+ const reason = `the scan failed (${error instanceof Error ? error.message.slice(0, 120) : String(error)})`;
310
+ return { ...emptyVulnResult(projectPath, true, { count: deps.length, reason }), ecosystemsScanned: [...new Set(deps.map((d) => d.ecosystem))] };
306
311
  }
307
312
  }
308
313
  /**
@@ -320,7 +325,7 @@ export class LiveIntelligence {
320
325
  if (this.warmup)
321
326
  return;
322
327
  this.warmupFailedAt = null;
323
- this.warmup = this.scanVulnerabilities(projectPath).catch(() => emptyVulnResult(projectPath, true));
328
+ this.warmup = this.scanVulnerabilities(projectPath).catch((error) => emptyVulnResult(projectPath, true, { count: this.auditDeps.length, reason: `the scan failed (${String(error).slice(0, 120)})` }));
324
329
  }
325
330
  /**
326
331
  * A usable vulnerability scan, or null. Re-resolves first when a lockfile
@@ -467,12 +472,12 @@ export class LiveIntelligence {
467
472
  */
468
473
  async queryAdvisories(deps) {
469
474
  if (!this.enabled)
470
- return emptyVulnResult("dependency_check", true);
475
+ return emptyVulnResult("dependency_check", true, { count: deps.length, reason: LOOKUPS_DISABLED });
471
476
  try {
472
477
  return await this.osvScanner.scan(deps, "dependency_check");
473
478
  }
474
- catch {
475
- return emptyVulnResult("dependency_check", true);
479
+ catch (error) {
480
+ return emptyVulnResult("dependency_check", true, { count: deps.length, reason: `the scan failed (${String(error).slice(0, 120)})` });
476
481
  }
477
482
  }
478
483
  getStatus() {
@@ -126,8 +126,12 @@ export function checkInstallState(dir, resolved, lockfilePath) {
126
126
  const manifest = findInstalledManifest(dir, dep.name, chain);
127
127
  if (manifest)
128
128
  signatures.set(manifest, fileSignature(manifest));
129
- const installedVersion = manifest ? readManifestVersion(manifest) : null;
130
- if (installedVersion && installedVersion !== dep.version) {
129
+ // A leading `v` is not part of the version: @upstash/* publish "v2.0.7",
130
+ // which read as drift from the lockfile's 2.0.7 (audit L1), and would have
131
+ // gone to OSV as "v2.0.7". The 4DA app drops it the same way.
132
+ const raw = manifest ? readManifestVersion(manifest) : null;
133
+ const installedVersion = raw ? raw.trim().replace(/^v(?=\d)/, "") : null;
134
+ if (installedVersion && installedVersion !== dep.version.replace(/^v(?=\d)/, "")) {
131
135
  drift.push({
132
136
  package: dep.name,
133
137
  dir,
@@ -64,10 +64,17 @@ export declare function readPnpmLock(content: string, versions: Map<string, stri
64
64
  * names the real package in `resolution:`; v1 only in the alias spec.
65
65
  */
66
66
  export declare function yarnPackageName(block: string, firstLine: string, headerName: string): string;
67
+ /** Lines longer than this in a yarn.lock are not read: no real entry comes near it. */
68
+ export declare const MAX_LOCKFILE_LINE = 4096;
69
+ /** The `version` of a yarn.lock block: v1 ` version "1.2.3"`, berry ` version: 1.2.3`. */
70
+ export declare function yarnBlockVersion(block: string): string | null;
67
71
  /**
68
72
  * Reads a yarn lockfile (v1 or berry) into `versions`.
69
73
  * v1: `"@scope/pkg@^1.0.0", "@scope/pkg@^1.1.0":` then ` version "1.2.3"`.
70
74
  * berry: `"@scope/pkg@npm:^1.0.0":` then ` version: 1.2.3`.
71
75
  * Workspace, link, portal, file, exec and patch entries do not name a registry version.
76
+ * Returns how many entries were skipped because a line of theirs is longer
77
+ * than MAX_LOCKFILE_LINE, so the caller can say the read was partial (audit
78
+ * L5: the skip was silent).
72
79
  */
73
- export declare function readYarnLock(content: string, versions: Map<string, string>, instances?: InstanceSet, specs?: Map<string, string>): void;
80
+ export declare function readYarnLock(content: string, versions: Map<string, string>, instances?: InstanceSet, specs?: Map<string, string>): number;