@4da/mcp-server 5.0.4 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +233 -37
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* dependency_check signal evaluation — pure functions, no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Each signal states a fact with its evidence and the verdict that fact
|
|
6
|
+
* implies on its own (`effect`). The item verdict is the strongest effect by
|
|
7
|
+
* a fixed precedence. `unknown` sits above `wait`/`proceed` and below
|
|
8
|
+
* `review`/`avoid`: a concrete negative finding stays actionable even when
|
|
9
|
+
* another source was unreachable, but a missing source never lets the
|
|
10
|
+
* verdict fall through to `proceed`.
|
|
11
|
+
*/
|
|
12
|
+
import { comparePrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
13
|
+
/** Verdict precedence, strongest first. */
|
|
14
|
+
const PRECEDENCE = ["avoid", "review", "unknown", "wait", "proceed"];
|
|
15
|
+
export const WAIT_DAYS = 3;
|
|
16
|
+
export const NEW_DEP_MIN_AGE_DAYS = 30;
|
|
17
|
+
const DAY_MS = 86_400_000;
|
|
18
|
+
/** The release `to` is judged against: the installed `from`, else the previous release on `to`'s line. */
|
|
19
|
+
export function selectBaseline(index, item) {
|
|
20
|
+
if (item.from) {
|
|
21
|
+
const rec = index.releases[item.from] ?? null;
|
|
22
|
+
return {
|
|
23
|
+
record: rec,
|
|
24
|
+
kind: "from",
|
|
25
|
+
note: rec ? `installed ${item.from}` : `installed version ${item.from} is not listed on the registry`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const target = parseSemverPrecedence(item.to);
|
|
29
|
+
if (!target)
|
|
30
|
+
return { record: null, kind: "previous_on_line", note: "target version is not readable semver" };
|
|
31
|
+
let best = null;
|
|
32
|
+
for (const rec of Object.values(index.releases)) {
|
|
33
|
+
if (rec.withdrawn)
|
|
34
|
+
continue;
|
|
35
|
+
const p = parseSemverPrecedence(rec.version);
|
|
36
|
+
if (!p || !sameLine(p, target) || comparePrecedence(p, target) >= 0)
|
|
37
|
+
continue;
|
|
38
|
+
if (p.prerelease.length > 0 && target.prerelease.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
if (!best || comparePrecedence(p, best.p) > 0)
|
|
41
|
+
best = { rec, p };
|
|
42
|
+
}
|
|
43
|
+
return best
|
|
44
|
+
? { record: best.rec, kind: "previous_on_line", note: `previous release on the same line, ${best.rec.version}` }
|
|
45
|
+
: { record: null, kind: "previous_on_line", note: "first release on its line; nothing earlier to compare" };
|
|
46
|
+
}
|
|
47
|
+
/** Same compatibility line: same major, or same 0.minor below 1.0. */
|
|
48
|
+
function sameLine(a, b) {
|
|
49
|
+
return a.major === b.major && (a.major !== 0 || a.minor === b.minor);
|
|
50
|
+
}
|
|
51
|
+
/** Runtime dependency names in `to` that the baseline did not have; null when either list is unknown. */
|
|
52
|
+
export function introducedDependencies(to, base) {
|
|
53
|
+
if (!to.dependencies || !base?.dependencies)
|
|
54
|
+
return null;
|
|
55
|
+
const before = new Set(base.dependencies);
|
|
56
|
+
return to.dependencies.filter((d) => !before.has(d)).sort();
|
|
57
|
+
}
|
|
58
|
+
export function advisoriesSignal(item, facts) {
|
|
59
|
+
if (!facts.available) {
|
|
60
|
+
return { id: "advisories", value: null, evidence: `OSV.dev could not be reached: ${facts.detail}`, effect: "unknown" };
|
|
61
|
+
}
|
|
62
|
+
const fromIds = new Set(facts.from.map((a) => a.id));
|
|
63
|
+
const toIds = new Set(facts.to.map((a) => a.id));
|
|
64
|
+
const fixed = facts.from.filter((a) => !toIds.has(a.id) && !a.maintenance);
|
|
65
|
+
const value = { affecting_to: facts.to.map((a) => a.id), fixed_by_this_change: fixed.map((a) => a.id) };
|
|
66
|
+
const fixedNote = fixed.length > 0 ? ` This change fixes ${list(fixed)} affecting ${item.from}.` : "";
|
|
67
|
+
const serious = facts.to.filter((a) => !a.maintenance && (a.severity === "critical" || a.severity === "high"));
|
|
68
|
+
if (serious.length > 0) {
|
|
69
|
+
return { id: "advisories", value, effect: "avoid", evidence: `${item.to} is affected by ${list(serious)}.${fixedNote}` };
|
|
70
|
+
}
|
|
71
|
+
const other = facts.to.filter((a) => !a.maintenance);
|
|
72
|
+
const newNotices = facts.to.filter((a) => a.maintenance && !fromIds.has(a.id));
|
|
73
|
+
if (other.length > 0 || newNotices.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
id: "advisories",
|
|
76
|
+
value,
|
|
77
|
+
effect: "review",
|
|
78
|
+
evidence: `${item.to} is affected by ${list([...other, ...newNotices])}.${fixedNote}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Only maintenance notices that `from` already carries can remain here.
|
|
82
|
+
const carried = facts.to.length > 0 ? ` (maintenance notice${facts.to.length > 1 ? "s" : ""} ${facts.to.map((a) => a.id).join(", ")} already apply to ${item.from})` : "";
|
|
83
|
+
return {
|
|
84
|
+
id: "advisories",
|
|
85
|
+
value,
|
|
86
|
+
effect: "proceed",
|
|
87
|
+
evidence: `No known advisory affects ${item.to}${carried}.${fixedNote}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function fmt(a) {
|
|
91
|
+
return `${a.id} (${a.maintenance ? "unmaintained notice" : a.severity})`;
|
|
92
|
+
}
|
|
93
|
+
/** At most three named in prose; the full id list is in the signal's `value`. */
|
|
94
|
+
function list(advisories) {
|
|
95
|
+
const shown = advisories.slice(0, 3).map(fmt).join(", ");
|
|
96
|
+
return advisories.length > 3 ? `${shown} and ${advisories.length - 3} more` : shown;
|
|
97
|
+
}
|
|
98
|
+
export function releaseAgeSignal(item, to, advisories, now) {
|
|
99
|
+
if (!to.publishedAt) {
|
|
100
|
+
return { id: "release_age", value: null, evidence: `The registry gives no publish time for ${item.to}.`, effect: "unknown" };
|
|
101
|
+
}
|
|
102
|
+
const days = (now.getTime() - Date.parse(to.publishedAt)) / DAY_MS;
|
|
103
|
+
const value = { days: Math.round(days * 10) / 10, published_at: to.publishedAt };
|
|
104
|
+
const ageText = `${item.to} was published ${value.days} day${value.days === 1 ? "" : "s"} ago (${to.publishedAt})`;
|
|
105
|
+
if (days >= WAIT_DAYS) {
|
|
106
|
+
return { id: "release_age", value, evidence: `${ageText}.`, effect: "proceed" };
|
|
107
|
+
}
|
|
108
|
+
const fixed = advisories.value?.fixed_by_this_change ?? [];
|
|
109
|
+
if (fixed.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
id: "release_age",
|
|
112
|
+
value,
|
|
113
|
+
effect: "proceed",
|
|
114
|
+
evidence: `${ageText}, under the ${WAIT_DAYS}-day hold, but it fixes ${fixed.join(", ")} affecting ${item.from}; a security fix is not held back.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: "release_age",
|
|
119
|
+
value,
|
|
120
|
+
effect: "wait",
|
|
121
|
+
evidence: `${ageText}; releases younger than ${WAIT_DAYS} days are held so a compromised publish can be caught and pulled first.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function publishTrustSignal(item, to, base) {
|
|
125
|
+
if (to.withdrawn === "unpublished") {
|
|
126
|
+
// Its publisher record is gone with it; comparing would report a "drop" we cannot see.
|
|
127
|
+
return { id: "publish_trust", value: null, evidence: `${item.to} was unpublished; its publish metadata is gone.`, effect: "info" };
|
|
128
|
+
}
|
|
129
|
+
if (!base.record) {
|
|
130
|
+
return {
|
|
131
|
+
id: "publish_trust",
|
|
132
|
+
value: { to: to.trust, baseline: null },
|
|
133
|
+
evidence: `${item.to}: ${to.trustEvidence}. No baseline: ${base.note}.`,
|
|
134
|
+
effect: "info",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const b = base.record;
|
|
138
|
+
const value = { to: to.trust, baseline: b.trust, baseline_version: b.version, baseline_kind: base.kind };
|
|
139
|
+
const evidence = `${item.to}: level ${to.trust}, ${to.trustEvidence}. ${b.version} (${base.note}): level ${b.trust}, ${b.trustEvidence}.`;
|
|
140
|
+
if (to.trust < b.trust) {
|
|
141
|
+
return { id: "publish_trust", value, effect: "review", evidence: `Publish trust dropped. ${evidence}` };
|
|
142
|
+
}
|
|
143
|
+
return { id: "publish_trust", value, effect: "proceed", evidence };
|
|
144
|
+
}
|
|
145
|
+
export function installScriptSignal(item, to, from) {
|
|
146
|
+
if (item.ecosystem !== "npm") {
|
|
147
|
+
return { id: "install_script_added", value: "not_applicable", evidence: "npm only (crates run build.rs at build time, which the registry does not expose).", effect: "info" };
|
|
148
|
+
}
|
|
149
|
+
const scripts = to.installScripts ?? [];
|
|
150
|
+
if (!item.from) {
|
|
151
|
+
return {
|
|
152
|
+
id: "install_script_added",
|
|
153
|
+
value: scripts,
|
|
154
|
+
effect: "info",
|
|
155
|
+
evidence: scripts.length > 0 ? `${item.to} runs install-time scripts: ${scripts.join(", ")}.` : `${item.to} declares no install-time scripts.`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (!from || from.installScripts === null) {
|
|
159
|
+
return { id: "install_script_added", value: null, effect: "unknown", evidence: `Install scripts of ${item.from} are unknown (${from ? "release metadata gone" : "not on the registry"}).` };
|
|
160
|
+
}
|
|
161
|
+
const added = scripts.filter((s) => !from.installScripts.includes(s));
|
|
162
|
+
if (added.length > 0) {
|
|
163
|
+
return {
|
|
164
|
+
id: "install_script_added",
|
|
165
|
+
value: added,
|
|
166
|
+
effect: "review",
|
|
167
|
+
evidence: `${item.to} adds install-time script${added.length > 1 ? "s" : ""} ${added.join(", ")} that ${item.from} did not have; it runs on every install.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
id: "install_script_added",
|
|
172
|
+
value: [],
|
|
173
|
+
effect: "proceed",
|
|
174
|
+
evidence: scripts.length > 0 ? `Install scripts unchanged (${scripts.join(", ")}).` : "Neither version declares install-time scripts.",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function newDependenciesSignal(to, base, introduced, created) {
|
|
178
|
+
if (!base.record) {
|
|
179
|
+
return { id: "new_dependencies", value: null, evidence: `No baseline to diff against: ${base.note}.`, effect: "info" };
|
|
180
|
+
}
|
|
181
|
+
if (introduced === null) {
|
|
182
|
+
return { id: "new_dependencies", value: null, evidence: `Dependency lists for ${to.version} or ${base.record.version} are unavailable.`, effect: "unknown" };
|
|
183
|
+
}
|
|
184
|
+
if (introduced.length === 0) {
|
|
185
|
+
return { id: "new_dependencies", value: [], evidence: `No runtime dependency added since ${base.record.version}.`, effect: "proceed" };
|
|
186
|
+
}
|
|
187
|
+
const ref = to.publishedAt ? Date.parse(to.publishedAt) : Date.now();
|
|
188
|
+
const rows = introduced.map((name) => {
|
|
189
|
+
const fact = created.get(name);
|
|
190
|
+
if (!fact)
|
|
191
|
+
return { name, first_published: null, age_days: null, note: "not checked (per-call lookup cap)" };
|
|
192
|
+
if (fact.status !== "ok")
|
|
193
|
+
return { name, first_published: null, age_days: null, note: fact.detail, status: fact.status };
|
|
194
|
+
const age = fact.createdAt ? Math.round(((ref - Date.parse(fact.createdAt)) / DAY_MS) * 10) / 10 : null;
|
|
195
|
+
return { name, first_published: fact.createdAt, age_days: age };
|
|
196
|
+
});
|
|
197
|
+
const young = rows.filter((r) => r.age_days !== null && r.age_days < NEW_DEP_MIN_AGE_DAYS);
|
|
198
|
+
const missing = rows.filter((r) => "status" in r && r.status === "not_found");
|
|
199
|
+
const gaps = rows.filter((r) => r.age_days === null && !("status" in r && r.status === "not_found"));
|
|
200
|
+
const list = rows.map((r) => (r.age_days !== null ? `${r.name} (first published ${r.age_days} days before ${to.version})` : `${r.name} (${r.note})`)).join(", ");
|
|
201
|
+
if (young.length > 0 || missing.length > 0) {
|
|
202
|
+
return {
|
|
203
|
+
id: "new_dependencies",
|
|
204
|
+
value: rows,
|
|
205
|
+
effect: "review",
|
|
206
|
+
evidence: `${to.version} adds ${list} since ${base.record.version}. A brand-new or vanished package as a new dependency is how injected-dependency compromises arrive.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id: "new_dependencies",
|
|
211
|
+
value: rows,
|
|
212
|
+
effect: gaps.length > 0 ? "unknown" : "proceed",
|
|
213
|
+
evidence: `${to.version} adds ${list} since ${base.record.version}.`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function withdrawnSignal(item, to) {
|
|
217
|
+
if (to.withdrawn) {
|
|
218
|
+
return { id: "yanked_or_deprecated", value: { [to.withdrawn]: true }, effect: "avoid", evidence: `${item.to} was ${to.withdrawn} from the registry.` };
|
|
219
|
+
}
|
|
220
|
+
if (to.deprecated) {
|
|
221
|
+
return { id: "yanked_or_deprecated", value: { deprecated: to.deprecated }, effect: "avoid", evidence: `${item.to} is deprecated: "${to.deprecated}".` };
|
|
222
|
+
}
|
|
223
|
+
return { id: "yanked_or_deprecated", value: null, effect: "proceed", evidence: `${item.to} is neither yanked nor deprecated.` };
|
|
224
|
+
}
|
|
225
|
+
export function upgradeTypeSignal(item) {
|
|
226
|
+
const kind = upgradeType(item.from, item.to);
|
|
227
|
+
const text = item.from ? `${item.from} -> ${item.to}: ${kind}` : `new dependency at ${item.to}`;
|
|
228
|
+
const zeroX = kind === "major" && /^0\./.test(item.from ?? "") ? " (0.x: a minor bump is breaking)" : "";
|
|
229
|
+
return { id: "upgrade_type", value: kind, evidence: `${text}${zeroX}.`, effect: "info" };
|
|
230
|
+
}
|
|
231
|
+
export function upgradeType(from, to) {
|
|
232
|
+
if (!from)
|
|
233
|
+
return "new_dependency";
|
|
234
|
+
const a = parseSemverPrecedence(from);
|
|
235
|
+
const b = parseSemverPrecedence(to);
|
|
236
|
+
if (!a || !b)
|
|
237
|
+
return "unknown";
|
|
238
|
+
const cmp = comparePrecedence(b, a);
|
|
239
|
+
if (cmp === 0)
|
|
240
|
+
return "none";
|
|
241
|
+
if (cmp < 0)
|
|
242
|
+
return "downgrade";
|
|
243
|
+
if (a.major !== b.major)
|
|
244
|
+
return "major";
|
|
245
|
+
if (a.minor !== b.minor)
|
|
246
|
+
return a.major === 0 ? "major" : "minor";
|
|
247
|
+
if (a.major === 0 && a.minor === 0 && a.patch !== b.patch)
|
|
248
|
+
return "major";
|
|
249
|
+
return "patch";
|
|
250
|
+
}
|
|
251
|
+
/** The item verdict and its one-line reason, from the strongest signal effect. */
|
|
252
|
+
export function decide(signals) {
|
|
253
|
+
for (const verdict of PRECEDENCE) {
|
|
254
|
+
const hits = signals.filter((s) => s.effect === verdict);
|
|
255
|
+
if (hits.length === 0)
|
|
256
|
+
continue;
|
|
257
|
+
if (verdict === "proceed") {
|
|
258
|
+
const fixes = signals.find((s) => s.id === "release_age" && /security fix/.test(s.evidence));
|
|
259
|
+
return { verdict, reason: fixes ? fixes.evidence : "No advisory, age, trust, script or new-dependency concern found." };
|
|
260
|
+
}
|
|
261
|
+
return { verdict, reason: hits.map((s) => `${s.id}: ${s.evidence}`).join(" | ") };
|
|
262
|
+
}
|
|
263
|
+
return { verdict: "unknown", reason: "No signal could be evaluated." };
|
|
264
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency_check tool
|
|
3
|
+
*
|
|
4
|
+
* A pre-flight verdict for ONE proposed change per item: adding a dependency
|
|
5
|
+
* at a version, or bumping an installed one. An agent calls this before it
|
|
6
|
+
* edits a manifest and gets `proceed | wait | review | avoid | unknown`, a
|
|
7
|
+
* one-line reason, and the signals behind it, each with evidence.
|
|
8
|
+
*
|
|
9
|
+
* Signals (see dependency-check-signals.ts): known advisories on the target
|
|
10
|
+
* (OSV), release age (hold releases < 3 days unless they fix an advisory on
|
|
11
|
+
* the installed version), publish-trust drop, newly added install scripts
|
|
12
|
+
* (npm), newly introduced runtime dependencies that are themselves < 30 days
|
|
13
|
+
* old, yanked/deprecated/unpublished targets, and the upgrade type.
|
|
14
|
+
*
|
|
15
|
+
* Honesty contract: a registry or OSV that cannot be reached makes the item
|
|
16
|
+
* `unknown`, never `proceed`. A package missing from the public registry is
|
|
17
|
+
* `unknown` with that said (private package, or a misspelled/hallucinated
|
|
18
|
+
* name), never treated as clean.
|
|
19
|
+
*
|
|
20
|
+
* Privacy: registry requests carry the package name only — the installed and
|
|
21
|
+
* target versions are picked out of the full release list locally. Only OSV
|
|
22
|
+
* receives name + version, exactly as vulnerability_scan already does.
|
|
23
|
+
*/
|
|
24
|
+
import type { FourDADatabase } from "../db.js";
|
|
25
|
+
import type { LiveIntelligence } from "../live/index.js";
|
|
26
|
+
import { type Signal, type Verdict } from "./dependency-check-signals.js";
|
|
27
|
+
export interface DependencyCheckParams {
|
|
28
|
+
items?: unknown;
|
|
29
|
+
}
|
|
30
|
+
export declare const dependencyCheckTool: {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object";
|
|
35
|
+
properties: {
|
|
36
|
+
items: {
|
|
37
|
+
type: string;
|
|
38
|
+
minItems: number;
|
|
39
|
+
maxItems: number;
|
|
40
|
+
description: string;
|
|
41
|
+
items: {
|
|
42
|
+
type: string;
|
|
43
|
+
properties: {
|
|
44
|
+
ecosystem: {
|
|
45
|
+
type: string;
|
|
46
|
+
enum: string[];
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
package: {
|
|
50
|
+
type: string;
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
to: {
|
|
54
|
+
type: string;
|
|
55
|
+
description: string;
|
|
56
|
+
};
|
|
57
|
+
from: {
|
|
58
|
+
type: string;
|
|
59
|
+
description: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
required: string[];
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
required: string[];
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
interface ItemResult {
|
|
70
|
+
ecosystem: string;
|
|
71
|
+
package: string;
|
|
72
|
+
from: string | null;
|
|
73
|
+
to: string;
|
|
74
|
+
/** "latest" when the caller asked for it and `to` is what it resolved to. */
|
|
75
|
+
to_requested?: string;
|
|
76
|
+
verdict: Verdict;
|
|
77
|
+
reason: string;
|
|
78
|
+
signals: Signal[];
|
|
79
|
+
}
|
|
80
|
+
export declare function executeDependencyCheck(_db: FourDADatabase, params: DependencyCheckParams, liveIntel: LiveIntelligence | null, opts?: {
|
|
81
|
+
now?: Date;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
checked_at: string;
|
|
84
|
+
summary: string;
|
|
85
|
+
results: ItemResult[];
|
|
86
|
+
_meta: {
|
|
87
|
+
sources: string[];
|
|
88
|
+
verdict_rule: string;
|
|
89
|
+
privacy: string;
|
|
90
|
+
offline: boolean;
|
|
91
|
+
osv_cached: boolean;
|
|
92
|
+
provenance: {
|
|
93
|
+
mode: string;
|
|
94
|
+
note: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
}>;
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* dependency_check tool
|
|
4
|
+
*
|
|
5
|
+
* A pre-flight verdict for ONE proposed change per item: adding a dependency
|
|
6
|
+
* at a version, or bumping an installed one. An agent calls this before it
|
|
7
|
+
* edits a manifest and gets `proceed | wait | review | avoid | unknown`, a
|
|
8
|
+
* one-line reason, and the signals behind it, each with evidence.
|
|
9
|
+
*
|
|
10
|
+
* Signals (see dependency-check-signals.ts): known advisories on the target
|
|
11
|
+
* (OSV), release age (hold releases < 3 days unless they fix an advisory on
|
|
12
|
+
* the installed version), publish-trust drop, newly added install scripts
|
|
13
|
+
* (npm), newly introduced runtime dependencies that are themselves < 30 days
|
|
14
|
+
* old, yanked/deprecated/unpublished targets, and the upgrade type.
|
|
15
|
+
*
|
|
16
|
+
* Honesty contract: a registry or OSV that cannot be reached makes the item
|
|
17
|
+
* `unknown`, never `proceed`. A package missing from the public registry is
|
|
18
|
+
* `unknown` with that said (private package, or a misspelled/hallucinated
|
|
19
|
+
* name), never treated as clean.
|
|
20
|
+
*
|
|
21
|
+
* Privacy: registry requests carry the package name only — the installed and
|
|
22
|
+
* target versions are picked out of the full release list locally. Only OSV
|
|
23
|
+
* receives name + version, exactly as vulnerability_scan already does.
|
|
24
|
+
*/
|
|
25
|
+
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
26
|
+
import { compareVersionPrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
27
|
+
import { advisoriesSignal, decide, installScriptSignal, introducedDependencies, newDependenciesSignal, publishTrustSignal, releaseAgeSignal, selectBaseline, upgradeTypeSignal, withdrawnSignal, } from "./dependency-check-signals.js";
|
|
28
|
+
const MAX_ITEMS = 25;
|
|
29
|
+
/** First-publish lookups for newly introduced dependencies, per call (each is a registry read). */
|
|
30
|
+
const MAX_NEW_DEP_LOOKUPS = 40;
|
|
31
|
+
const NPM_CONCURRENCY = 4;
|
|
32
|
+
const PRIVACY_NOTE = "Registry requests carried package names only (npm full packument, crates.io versions API and sparse index); " +
|
|
33
|
+
"installed and target versions were never sent to a registry. OSV.dev received package names and versions, as vulnerability_scan does.";
|
|
34
|
+
const VERDICT_RULE = "avoid > review > unknown > wait > proceed. unknown = a registry or OSV.dev lookup failed (never read as proceed); " +
|
|
35
|
+
"a concrete avoid/review finding still wins over a missing source.";
|
|
36
|
+
export const dependencyCheckTool = {
|
|
37
|
+
name: "dependency_check",
|
|
38
|
+
description: [
|
|
39
|
+
"Pre-flight verdict for adding a dependency or changing one to a specific version. Call BEFORE you add a package to a manifest or apply a version bump (including bumps proposed by upgrade_planner or a bot), and act on the verdict.",
|
|
40
|
+
"",
|
|
41
|
+
"Per item it returns `verdict` (proceed | wait | review | avoid | unknown), a one-line `reason`, and `signals`, each with `id`, `value`, `evidence` and the verdict that signal implies alone (`effect`):",
|
|
42
|
+
"- advisories: known OSV advisories affecting `to`, and those this change fixes on `from`. High/critical on `to` -> avoid; other -> review.",
|
|
43
|
+
"- release_age: days since `to` was published. Under 3 days -> wait, unless `to` fixes an advisory affecting `from` (a security fix is not held).",
|
|
44
|
+
"- publish_trust: trust level of `to` vs `from` (or, for a new dependency, the previous release on the same line). 2 = trusted publisher (npm `_npmUser.trustedPublisher`, crates.io `trustpub_data`), 1 = npm provenance attestation, 0 = neither. A drop -> review.",
|
|
45
|
+
"- install_script_added (npm): `to` runs preinstall/install/postinstall and `from` did not -> review. For a new dependency, information only.",
|
|
46
|
+
"- new_dependencies: runtime dependencies added since the baseline, each with its first-publish age. Any under 30 days old, or no longer on the registry -> review.",
|
|
47
|
+
"- yanked_or_deprecated: `to` yanked, unpublished or deprecated -> avoid.",
|
|
48
|
+
"- upgrade_type: patch | minor | major (a 0.x minor counts as major) | downgrade | new_dependency. Information only.",
|
|
49
|
+
"",
|
|
50
|
+
"Verdict precedence: avoid > review > unknown > wait > proceed. `unknown` means a registry or OSV.dev lookup failed, or the package is not on the public registry (private package, or a misspelled/hallucinated name); it is never a silent proceed. Apply only `proceed` items; report the others with their evidence. For WHAT the bump changes in your code, call upgrade_impact.",
|
|
51
|
+
"",
|
|
52
|
+
"Privacy: registry requests carry the package NAME only (npm full packument, crates.io versions API and sparse index); the installed and target versions are never sent to a registry. OSV.dev receives package names and versions, as vulnerability_scan does. Set FOURDA_OFFLINE=true to disable all lookups (every item then returns unknown).",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
items: {
|
|
58
|
+
type: "array",
|
|
59
|
+
minItems: 1,
|
|
60
|
+
maxItems: 25,
|
|
61
|
+
description: "The proposed changes to check, 1 to 25.",
|
|
62
|
+
items: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
ecosystem: { type: "string", enum: ["npm", "crates.io"], description: "Package registry." },
|
|
66
|
+
package: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: 'Package name exactly as published (e.g. "axios", "@scope/pkg", "serde_json").',
|
|
69
|
+
},
|
|
70
|
+
to: { type: "string", description: 'Exact version you intend to install (e.g. "1.14.1"), or "latest" for the newest stable release; not a range.' },
|
|
71
|
+
from: { type: "string", description: "Exact version currently installed. Omit when adding a new dependency." },
|
|
72
|
+
},
|
|
73
|
+
required: ["ecosystem", "package", "to"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["items"],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
export async function executeDependencyCheck(_db, params, liveIntel, opts = {}) {
|
|
81
|
+
const requested = validateItems(params.items);
|
|
82
|
+
const now = opts.now ?? new Date();
|
|
83
|
+
const checkedAt = now.toISOString();
|
|
84
|
+
if (!liveIntel || !liveIntel.isEnabled()) {
|
|
85
|
+
const reason = "Live lookups are disabled (FOURDA_OFFLINE=true); nothing about these versions could be checked.";
|
|
86
|
+
return finish(checkedAt, requested.map((item) => ({ ...head(item), verdict: "unknown", reason, signals: [] })), { offline: true, osvCached: false });
|
|
87
|
+
}
|
|
88
|
+
const sources = liveIntel.getReleaseSources();
|
|
89
|
+
const indexes = await fetchIndexes(requested, sources);
|
|
90
|
+
// `to: "latest"` becomes the newest stable release the registry lists. Agents
|
|
91
|
+
// adding a package pass it naturally; rejecting it cost a retry in 7 of 24
|
|
92
|
+
// dependency_check trials of the 2026-10-03 agent eval.
|
|
93
|
+
const items = requested.map((item) => resolveLatest(item, indexes.get(lookupKey(item))));
|
|
94
|
+
const osvDeps = items.flatMap((item) => [item.to, item.from].filter((v) => Boolean(v)).map((version) => osvDep(item, version)));
|
|
95
|
+
const scan = await liveIntel.queryAdvisories(osvDeps);
|
|
96
|
+
const byInstance = new Map();
|
|
97
|
+
for (const v of scan.vulnerabilities) {
|
|
98
|
+
const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
|
|
99
|
+
byInstance.set(key, [...(byInstance.get(key) ?? []), v]);
|
|
100
|
+
}
|
|
101
|
+
const advisoriesFor = (item) => scan.offline
|
|
102
|
+
? { available: false, detail: "lookup failed or was rate limited" }
|
|
103
|
+
: {
|
|
104
|
+
available: true,
|
|
105
|
+
to: refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.to))),
|
|
106
|
+
from: item.from ? refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.from))) : [],
|
|
107
|
+
};
|
|
108
|
+
// Phase 2: first-publish dates of newly introduced dependencies.
|
|
109
|
+
const plans = items.map((item) => planItem(item, indexes.get(lookupKey(item))));
|
|
110
|
+
const wantedCreated = new Map();
|
|
111
|
+
for (const p of plans) {
|
|
112
|
+
for (const name of p.introduced ?? []) {
|
|
113
|
+
if (wantedCreated.size >= MAX_NEW_DEP_LOOKUPS)
|
|
114
|
+
break;
|
|
115
|
+
wantedCreated.set(`${p.item.ecosystem}\0${name}`, { ecosystem: p.item.ecosystem, name });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const created = new Map();
|
|
119
|
+
await mapLimit([...wantedCreated.entries()], NPM_CONCURRENCY, async ([key, { ecosystem, name }]) => {
|
|
120
|
+
const lookup = await sources[ecosystem].getCreatedAt(name);
|
|
121
|
+
created.set(key, lookup.status === "ok" ? { status: "ok", createdAt: lookup.data } : lookup);
|
|
122
|
+
});
|
|
123
|
+
const results = plans.map((p) => {
|
|
124
|
+
if (p.blocked) {
|
|
125
|
+
// No release metadata: only the advisory lookup can still speak, and
|
|
126
|
+
// only a negative finding is worth more than "unknown".
|
|
127
|
+
const advisories = advisoriesSignal(p.item, advisoriesFor(p.item));
|
|
128
|
+
const negative = advisories.effect === "avoid" || advisories.effect === "review";
|
|
129
|
+
return {
|
|
130
|
+
...head(p.item),
|
|
131
|
+
verdict: negative ? advisories.effect : "unknown",
|
|
132
|
+
reason: negative ? `advisories: ${advisories.evidence} | ${p.blocked}` : p.blocked,
|
|
133
|
+
signals: [advisories],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const { item, index } = p;
|
|
137
|
+
const to = index.releases[item.to];
|
|
138
|
+
const base = selectBaseline(index, item);
|
|
139
|
+
const advisories = advisoriesSignal(item, advisoriesFor(item));
|
|
140
|
+
const createdForItem = new Map();
|
|
141
|
+
for (const name of p.introduced ?? []) {
|
|
142
|
+
const fact = created.get(`${item.ecosystem}\0${name}`);
|
|
143
|
+
if (fact)
|
|
144
|
+
createdForItem.set(name, fact);
|
|
145
|
+
}
|
|
146
|
+
const signals = [
|
|
147
|
+
advisories,
|
|
148
|
+
releaseAgeSignal(item, to, advisories, now),
|
|
149
|
+
publishTrustSignal(item, to, base),
|
|
150
|
+
installScriptSignal(item, to, item.from ? index.releases[item.from] ?? null : null),
|
|
151
|
+
newDependenciesSignal(to, base, p.introduced, createdForItem),
|
|
152
|
+
withdrawnSignal(item, to),
|
|
153
|
+
upgradeTypeSignal(item),
|
|
154
|
+
];
|
|
155
|
+
return { ...head(item), ...decide(signals), signals };
|
|
156
|
+
});
|
|
157
|
+
return finish(checkedAt, results, { offline: scan.offline, osvCached: scan.cached });
|
|
158
|
+
}
|
|
159
|
+
function planItem(item, lookup) {
|
|
160
|
+
if (lookup.status === "unreachable") {
|
|
161
|
+
return { item, blocked: `Registry could not be reached (${lookup.detail}); nothing about ${item.to} was verified.`, index: null, introduced: null };
|
|
162
|
+
}
|
|
163
|
+
if (lookup.status === "not_found") {
|
|
164
|
+
return {
|
|
165
|
+
item,
|
|
166
|
+
blocked: `${lookup.detail}. If it is a private package this tool cannot judge it; otherwise check the name for a typo or a hallucinated package before adding it.`,
|
|
167
|
+
index: null,
|
|
168
|
+
introduced: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const index = lookup.data;
|
|
172
|
+
const to = index.releases[item.to];
|
|
173
|
+
if (!to) {
|
|
174
|
+
return { item, blocked: `${item.package}@${item.to} is not a published version on ${item.ecosystem}.`, index, introduced: null };
|
|
175
|
+
}
|
|
176
|
+
return { item, blocked: null, index, introduced: introducedDependencies(to, selectBaseline(index, item).record) };
|
|
177
|
+
}
|
|
178
|
+
async function fetchIndexes(items, sources) {
|
|
179
|
+
const wanted = new Map();
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
const key = lookupKey(item);
|
|
182
|
+
const entry = wanted.get(key) ?? { item, versions: new Set() };
|
|
183
|
+
entry.versions.add(item.to);
|
|
184
|
+
if (item.from)
|
|
185
|
+
entry.versions.add(item.from);
|
|
186
|
+
wanted.set(key, entry);
|
|
187
|
+
}
|
|
188
|
+
const out = new Map();
|
|
189
|
+
await mapLimit([...wanted.entries()], NPM_CONCURRENCY, async ([key, { item, versions }]) => {
|
|
190
|
+
out.set(key, await sources[item.ecosystem].getReleases(item.package, [...versions]));
|
|
191
|
+
});
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function validateItems(raw) {
|
|
195
|
+
if (!Array.isArray(raw) || raw.length < 1 || raw.length > MAX_ITEMS) {
|
|
196
|
+
throw new Error(`items must be an array of 1 to ${MAX_ITEMS} { ecosystem, package, to, from? } objects`);
|
|
197
|
+
}
|
|
198
|
+
return raw.map((r, i) => {
|
|
199
|
+
const o = (r ?? {});
|
|
200
|
+
const where = `items[${i}]`;
|
|
201
|
+
if (o.ecosystem !== "npm" && o.ecosystem !== "crates.io")
|
|
202
|
+
throw new Error(`${where}.ecosystem must be "npm" or "crates.io"`);
|
|
203
|
+
if (typeof o.package !== "string" || !o.package.trim())
|
|
204
|
+
throw new Error(`${where}.package must be a non-empty string`);
|
|
205
|
+
const to = typeof o.to === "string" && o.to.trim().toLowerCase() === LATEST ? LATEST : exactVersion(o.to, `${where}.to`);
|
|
206
|
+
const from = o.from === undefined || o.from === null || o.from === "" ? undefined : exactVersion(o.from, `${where}.from`);
|
|
207
|
+
return { ecosystem: o.ecosystem, package: o.package.trim(), to, ...(from ? { from } : {}) };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function exactVersion(v, where) {
|
|
211
|
+
const s = typeof v === "string" ? v.trim().replace(/^v(?=\d)/, "") : "";
|
|
212
|
+
if (!parseSemverPrecedence(s)) {
|
|
213
|
+
const latest = where.endsWith(".to") ? ' or "latest"' : "";
|
|
214
|
+
throw new Error(`${where} must be an exact version like 1.2.3${latest} (got ${JSON.stringify(v)}), not a range`);
|
|
215
|
+
}
|
|
216
|
+
return s;
|
|
217
|
+
}
|
|
218
|
+
const LATEST = "latest";
|
|
219
|
+
/**
|
|
220
|
+
* `to: "latest"` -> the highest release that is neither a prerelease nor
|
|
221
|
+
* withdrawn (a deprecated latest stays: deprecation is a finding to report).
|
|
222
|
+
* Left as "latest" when the registry read failed or lists no release; the item
|
|
223
|
+
* is then blocked with the registry's own reason, or "not a published version".
|
|
224
|
+
*/
|
|
225
|
+
function resolveLatest(item, lookup) {
|
|
226
|
+
if (item.to !== LATEST || lookup.status !== "ok")
|
|
227
|
+
return item;
|
|
228
|
+
const candidates = Object.values(lookup.data.releases).filter((r) => !r.withdrawn && parseSemverPrecedence(r.version));
|
|
229
|
+
const stable = candidates.filter((r) => parseSemverPrecedence(r.version).prerelease.length === 0);
|
|
230
|
+
const pool = stable.length > 0 ? stable : candidates;
|
|
231
|
+
const newest = pool.reduce((max, r) => (max === null || (compareVersionPrecedence(r.version, max) ?? 0) > 0 ? r.version : max), null);
|
|
232
|
+
return newest ? { ...item, to: newest, requested: LATEST } : item;
|
|
233
|
+
}
|
|
234
|
+
function osvDep(item, version) {
|
|
235
|
+
return {
|
|
236
|
+
name: item.package,
|
|
237
|
+
version,
|
|
238
|
+
ecosystem: item.ecosystem,
|
|
239
|
+
isDev: false,
|
|
240
|
+
isDirect: true,
|
|
241
|
+
devScopeKnown: true,
|
|
242
|
+
target: null,
|
|
243
|
+
platformActive: true,
|
|
244
|
+
sourceDirs: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function refs(entries) {
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
return (entries ?? []).flatMap((v) => {
|
|
250
|
+
if (seen.has(v.vulnId))
|
|
251
|
+
return [];
|
|
252
|
+
seen.add(v.vulnId);
|
|
253
|
+
return [{ id: v.vulnId, severity: v.severity, summary: v.summary, maintenance: isMaintenanceNotice(v) }];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const instanceKey = (eco, name, version) => `${eco}\0${name}\0${version}`;
|
|
257
|
+
const lookupKey = (item) => `${item.ecosystem}\0${item.package}`;
|
|
258
|
+
const head = (item) => ({
|
|
259
|
+
ecosystem: item.ecosystem,
|
|
260
|
+
package: item.package,
|
|
261
|
+
from: item.from ?? null,
|
|
262
|
+
to: item.to,
|
|
263
|
+
...(item.requested ? { to_requested: item.requested } : {}),
|
|
264
|
+
});
|
|
265
|
+
function finish(checkedAt, results, meta) {
|
|
266
|
+
const counts = new Map();
|
|
267
|
+
for (const r of results)
|
|
268
|
+
counts.set(r.verdict, (counts.get(r.verdict) ?? 0) + 1);
|
|
269
|
+
const order = ["avoid", "review", "unknown", "wait", "proceed"];
|
|
270
|
+
const parts = order.filter((v) => counts.has(v)).map((v) => `${counts.get(v)} ${v}`);
|
|
271
|
+
return {
|
|
272
|
+
checked_at: checkedAt,
|
|
273
|
+
summary: `${results.length} checked: ${parts.join(", ")}. Apply only "proceed" items; report the rest with their evidence.`,
|
|
274
|
+
results,
|
|
275
|
+
_meta: {
|
|
276
|
+
sources: ["registry.npmjs.org", "crates.io", "index.crates.io", "osv.dev"],
|
|
277
|
+
verdict_rule: VERDICT_RULE,
|
|
278
|
+
privacy: PRIVACY_NOTE,
|
|
279
|
+
offline: meta.offline,
|
|
280
|
+
osv_cached: meta.osvCached,
|
|
281
|
+
provenance: {
|
|
282
|
+
mode: "live_registry_check",
|
|
283
|
+
note: "Point-in-time facts from the public registries and OSV.dev at checked_at. Says nothing about code behaviour; a proceed is the absence of known warning signs, not a review of the release.",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function mapLimit(items, limit, fn) {
|
|
289
|
+
let next = 0;
|
|
290
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
291
|
+
while (next < items.length)
|
|
292
|
+
await fn(items[next++]);
|
|
293
|
+
});
|
|
294
|
+
await Promise.all(workers);
|
|
295
|
+
}
|