@4da/mcp-server 5.0.3 → 5.0.4
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/dist/db.js +11 -1
- package/dist/live/osv-scanner.d.ts +20 -1
- package/dist/live/osv-scanner.js +82 -8
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -836,7 +836,17 @@ export class FourDADatabase {
|
|
|
836
836
|
query += ` AND source_type = ?`;
|
|
837
837
|
params.push(sourceType);
|
|
838
838
|
}
|
|
839
|
-
|
|
839
|
+
// Ranked read (desktop audit items 12+26, schema 110): order by the
|
|
840
|
+
// batch-relative rank when the analysis cycle has ranked the item, falling
|
|
841
|
+
// back to the evidence score. This mirrors the Rust source of truth —
|
|
842
|
+
// `RANKED_ORDER_EXPR` in `src-tauri/src/db/scoring_queries.rs` — and must
|
|
843
|
+
// stay textually in sync with it. The `relevance_score >= ?` membership
|
|
844
|
+
// filter above deliberately stays on evidence: evidence decides membership,
|
|
845
|
+
// rank decides order. Guarded on column existence for pre-110 DBs.
|
|
846
|
+
const rankedOrder = this.hasColumn("source_items", "rank_score")
|
|
847
|
+
? `COALESCE(rank_score, relevance_score) DESC`
|
|
848
|
+
: `relevance_score DESC`;
|
|
849
|
+
query += ` ORDER BY ${rankedOrder} LIMIT ?`;
|
|
840
850
|
// Over-fetch so near-duplicate collapsing below can still fill `limit`
|
|
841
851
|
// (the same story arrives via reddit + rss + mastodon: live feeds showed
|
|
842
852
|
// three copies of "Rust 1.98.0" in one 20-item response).
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { LiveCache } from "./cache.js";
|
|
10
10
|
import type { RateLimiter } from "./rate-limiter.js";
|
|
11
|
-
import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
|
|
11
|
+
import type { ResolvedDependency, OsvVulnerability, VulnerabilityScanResult } from "./types.js";
|
|
12
12
|
export declare class OsvScanner {
|
|
13
13
|
private cache;
|
|
14
14
|
private rateLimiter;
|
|
@@ -28,3 +28,22 @@ export declare function extractCvssScore(severity: Array<{
|
|
|
28
28
|
type: string;
|
|
29
29
|
score: string;
|
|
30
30
|
}> | undefined): number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Pick the fix version for the OSV range that actually CONTAINS the installed
|
|
33
|
+
* version — never another release line's fix. The old code returned the first
|
|
34
|
+
* range's first `fixed` event, so a multi-range advisory (e.g. undici fixed at
|
|
35
|
+
* 6.x on the 6 line and 7.x on the 7 line) recommended a DOWNGRADE to 6.x for
|
|
36
|
+
* an installed 7.x.
|
|
37
|
+
*
|
|
38
|
+
* Selection order:
|
|
39
|
+
* 1. The `fixed` of the interval containing installed (introduced <= installed
|
|
40
|
+
* < fixed).
|
|
41
|
+
* 2. If that interval has no fix (or none contains installed), the smallest
|
|
42
|
+
* published fix strictly ABOVE installed across all ranges.
|
|
43
|
+
* 3. If the installed version is unparseable, the highest published fix (it
|
|
44
|
+
* resolves the advisory regardless of release line).
|
|
45
|
+
* 4. Otherwise null — omitting the recommendation beats emitting a downgrade.
|
|
46
|
+
*
|
|
47
|
+
* Hard guard: the returned version is never below the installed version.
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractFixedVersion(affected: OsvVulnerability["affected"] | undefined, packageName: string, ecosystem: string, installedVersion: string | null): string | null;
|
package/dist/live/osv-scanner.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout, fetchJson } from "./http-utils.js";
|
|
11
11
|
import { cvssBaseScore } from "./cvss.js";
|
|
12
|
+
import { compareSemver, maxSemver, parseSemver } from "./semver-utils.js";
|
|
12
13
|
const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
13
14
|
const OSV_VULN_URL = "https://api.osv.dev/v1/vulns/";
|
|
14
15
|
const OSV_TIMEOUT_MS = 15_000;
|
|
@@ -174,7 +175,7 @@ export class OsvScanner {
|
|
|
174
175
|
}
|
|
175
176
|
function mapVulnerability(vuln, dep) {
|
|
176
177
|
const { severity, cvssScore } = deriveSeverity(vuln);
|
|
177
|
-
const fixedVersion = extractFixedVersion(vuln.affected, dep.name, dep.ecosystem);
|
|
178
|
+
const fixedVersion = extractFixedVersion(vuln.affected, dep.name, dep.ecosystem, dep.version);
|
|
178
179
|
return {
|
|
179
180
|
package: dep.name,
|
|
180
181
|
currentVersion: dep.version || "unknown",
|
|
@@ -260,18 +261,91 @@ function cvssToSeverity(score) {
|
|
|
260
261
|
return "low";
|
|
261
262
|
return "unknown";
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Pick the fix version for the OSV range that actually CONTAINS the installed
|
|
266
|
+
* version — never another release line's fix. The old code returned the first
|
|
267
|
+
* range's first `fixed` event, so a multi-range advisory (e.g. undici fixed at
|
|
268
|
+
* 6.x on the 6 line and 7.x on the 7 line) recommended a DOWNGRADE to 6.x for
|
|
269
|
+
* an installed 7.x.
|
|
270
|
+
*
|
|
271
|
+
* Selection order:
|
|
272
|
+
* 1. The `fixed` of the interval containing installed (introduced <= installed
|
|
273
|
+
* < fixed).
|
|
274
|
+
* 2. If that interval has no fix (or none contains installed), the smallest
|
|
275
|
+
* published fix strictly ABOVE installed across all ranges.
|
|
276
|
+
* 3. If the installed version is unparseable, the highest published fix (it
|
|
277
|
+
* resolves the advisory regardless of release line).
|
|
278
|
+
* 4. Otherwise null — omitting the recommendation beats emitting a downgrade.
|
|
279
|
+
*
|
|
280
|
+
* Hard guard: the returned version is never below the installed version.
|
|
281
|
+
*/
|
|
282
|
+
export function extractFixedVersion(affected, packageName, ecosystem, installedVersion) {
|
|
264
283
|
if (!affected)
|
|
265
284
|
return null;
|
|
285
|
+
const intervals = collectVulnerableIntervals(affected, packageName, ecosystem);
|
|
286
|
+
const fixes = intervals
|
|
287
|
+
.map((i) => i.fixed)
|
|
288
|
+
.filter((f) => f !== null);
|
|
289
|
+
if (fixes.length === 0)
|
|
290
|
+
return null;
|
|
291
|
+
if (installedVersion === null || parseSemver(installedVersion) === null) {
|
|
292
|
+
// Can't place the installed version on a release line — recommend the
|
|
293
|
+
// highest published fix so the upgrade resolves the advisory either way.
|
|
294
|
+
return maxSemver(fixes);
|
|
295
|
+
}
|
|
296
|
+
// The interval whose release line the installed version sits on. `compareSemver`
|
|
297
|
+
// returns 0 for unparseable bounds (e.g. introduced "0" = since inception),
|
|
298
|
+
// which keeps those intervals eligible on the introduced side only.
|
|
299
|
+
const containing = intervals.find((i) => compareSemver(installedVersion, i.introduced) >= 0 &&
|
|
300
|
+
(i.fixed === null || compareSemver(installedVersion, i.fixed) < 0));
|
|
301
|
+
if (containing?.fixed &&
|
|
302
|
+
compareSemver(containing.fixed, installedVersion) > 0) {
|
|
303
|
+
return containing.fixed;
|
|
304
|
+
}
|
|
305
|
+
// No fix on the installed line (open interval, or installed already past
|
|
306
|
+
// every containing range): smallest fix strictly above installed, or nothing.
|
|
307
|
+
const above = fixes.filter((f) => compareSemver(f, installedVersion) > 0);
|
|
308
|
+
if (above.length === 0)
|
|
309
|
+
return null;
|
|
310
|
+
return above.reduce((min, v) => (compareSemver(v, min) < 0 ? v : min), above[0]);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Flatten the matching package's version ranges into [introduced, fixed)
|
|
314
|
+
* intervals. OSV sorts events within a range; each `introduced` opens an
|
|
315
|
+
* interval and the next `fixed` closes it. GIT ranges hold commit hashes,
|
|
316
|
+
* not versions, and are skipped.
|
|
317
|
+
*/
|
|
318
|
+
function collectVulnerableIntervals(affected, packageName, ecosystem) {
|
|
319
|
+
const intervals = [];
|
|
266
320
|
for (const a of affected) {
|
|
267
|
-
if (a.package.name
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
321
|
+
if (a.package.name !== packageName || a.package.ecosystem !== ecosystem)
|
|
322
|
+
continue;
|
|
323
|
+
for (const range of a.ranges || []) {
|
|
324
|
+
if (range.type === "GIT")
|
|
325
|
+
continue;
|
|
326
|
+
let open = null;
|
|
327
|
+
for (const event of range.events || []) {
|
|
328
|
+
if (event.introduced !== undefined) {
|
|
329
|
+
if (open)
|
|
330
|
+
intervals.push(open);
|
|
331
|
+
open = { introduced: event.introduced, fixed: null };
|
|
332
|
+
}
|
|
333
|
+
else if (event.fixed !== undefined) {
|
|
334
|
+
if (open) {
|
|
335
|
+
open.fixed = event.fixed;
|
|
336
|
+
intervals.push(open);
|
|
337
|
+
open = null;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// `fixed` with no preceding `introduced` — treat as vulnerable
|
|
341
|
+
// since inception ("0" = OSV's since-the-beginning sentinel).
|
|
342
|
+
intervals.push({ introduced: "0", fixed: event.fixed });
|
|
343
|
+
}
|
|
272
344
|
}
|
|
273
345
|
}
|
|
346
|
+
if (open)
|
|
347
|
+
intervals.push(open);
|
|
274
348
|
}
|
|
275
349
|
}
|
|
276
|
-
return
|
|
350
|
+
return intervals;
|
|
277
351
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4da/mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"mcpName": "io.github.4DA-Systems/4da-mcp-server",
|
|
5
5
|
"description": "Dependency intelligence for AI coding agents. Live CVE scanning, dependency health, upgrade planning, ecosystem news, decision memory — zero config, privacy-first. 9 tools standalone, 14 total with desktop app.",
|
|
6
6
|
"type": "module",
|