@4da/mcp-server 5.0.3 → 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 +242 -36
- 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 +31 -1
- package/dist/live/osv-scanner.js +119 -10
- 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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge-gap grading: the pure rules that decide whether an unread item is
|
|
3
|
+
* evidence about a dependency and how severe the gap is. The database walk
|
|
4
|
+
* lives in knowledge-gaps.ts; advisory ranges in knowledge-gap-ranges.ts.
|
|
5
|
+
*/
|
|
6
|
+
import type { AdvisoryTier } from "./knowledge-gap-ranges.js";
|
|
7
|
+
export declare function mentionsPackage(text: string, pkg: string): boolean;
|
|
8
|
+
/** A knowledge gap's severity. */
|
|
9
|
+
export type GapSeverity = "critical" | "high" | "medium" | "low";
|
|
10
|
+
/** The subset of an item this grading needs. */
|
|
11
|
+
export interface GradableItem {
|
|
12
|
+
title: string | null;
|
|
13
|
+
source_type?: string | null;
|
|
14
|
+
content_type?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Set by a caller that has already decided whether this item cites the
|
|
17
|
+
* dependency: the dependency linker's structured proof says what a title
|
|
18
|
+
* cannot. Unset, the title decides.
|
|
19
|
+
*/
|
|
20
|
+
cites?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Package-registry sources: every row is a published version of a package. */
|
|
23
|
+
export declare const REGISTRY_SOURCES: Set<string>;
|
|
24
|
+
/** Advisory sources: every row is a security advisory. */
|
|
25
|
+
export declare const ADVISORY_SOURCES: Set<string>;
|
|
26
|
+
/**
|
|
27
|
+
* A registry advisory row (`knowledge_decay::is_advisory_row`). An editorial
|
|
28
|
+
* story the pipeline typed `security_advisory` is not one: it cites by title
|
|
29
|
+
* like any other story and is never proof of exposure (AD-040 rule 4).
|
|
30
|
+
*/
|
|
31
|
+
export declare function isAdvisoryRow(item: GradableItem): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The subject package of an advisory title in the ingested "[ID] package:
|
|
34
|
+
* summary" shape ("[GHSA-h395-gr6q-cpjc] jsonwebtoken: ..."), or null when
|
|
35
|
+
* the title has another shape and cannot be read that way.
|
|
36
|
+
*/
|
|
37
|
+
export declare function advisorySubject(title: string): string | null;
|
|
38
|
+
/** Same package name, ignoring case and the crates.io `-`/`_` split. */
|
|
39
|
+
export declare function samePackage(a: string, b: string): boolean;
|
|
40
|
+
/** The released version a registry row announces ("npm: react v19.3.0" → "19.3.0"), if any. */
|
|
41
|
+
export declare function registryVersionFromTitle(title: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Is an install inside a known advisory range?
|
|
44
|
+
* - exposed: a readable same-ecosystem advisory range contains the version
|
|
45
|
+
* - safe: every readable same-ecosystem range excludes it
|
|
46
|
+
* - unknown: nothing readable stored for its ecosystem, or no readable
|
|
47
|
+
* installed version — no claim either way
|
|
48
|
+
*/
|
|
49
|
+
export type Exposure = "exposed" | "safe" | "unknown";
|
|
50
|
+
/** Parse a source_items.published_at value (SQLite "YYYY-MM-DD HH:MM:SS" or ISO). */
|
|
51
|
+
export declare function parsePublishedAt(value: string | null | undefined): number | null;
|
|
52
|
+
/**
|
|
53
|
+
* Grade a knowledge gap by CONSEQUENCE, never by volume, on the tiers the
|
|
54
|
+
* desktop app draws (`knowledge_decay::classify_severity`):
|
|
55
|
+
*
|
|
56
|
+
* - `critical`: an advisory citing the dependency still reaches an install,
|
|
57
|
+
* and the most severe advisory reaching an install is critical or high by
|
|
58
|
+
* its OWN grade — CVSS band, else the source's curated label (AD-040 rule 2).
|
|
59
|
+
* - `high`: the same with a medium, low or ungraded advisory; or a title
|
|
60
|
+
* naming the dependency with a breaking change, deprecation or end of life.
|
|
61
|
+
* - `medium`: a registry release newer than an install, or a title naming the
|
|
62
|
+
* dependency with a release, update or upgrade.
|
|
63
|
+
* - `low`: everything else, including a large pile of passing mentions.
|
|
64
|
+
*
|
|
65
|
+
* `critical` used to mean "an advisory names the dependency", whatever the
|
|
66
|
+
* advisory's own tier: the jsonwebtoken crate's GHSA-h395-gr6q-cpjc, graded
|
|
67
|
+
* medium by its source and High by the app, was a critical gap here (measured
|
|
68
|
+
* 2026-09-11). Earlier still, `medium` meant "3+ recent unread mentions"
|
|
69
|
+
* matched on the content body: a `tracing` gap evidenced by "The Matrix:
|
|
70
|
+
* Writing Code That Doesn't Need Comments", a `typescript` gap by a
|
|
71
|
+
* Databricks job posting, fourteen of fifteen gaps noise. Two implementations
|
|
72
|
+
* of one concept disagreeing is what this mirrors the app to avoid.
|
|
73
|
+
*/
|
|
74
|
+
export declare function gradeGap(items: GradableItem[], packageName: string,
|
|
75
|
+
/**
|
|
76
|
+
* An advisory citing this dependency still reaches an install, or cannot be
|
|
77
|
+
* ruled out. False when every such advisory is fixed at or below the
|
|
78
|
+
* installed versions: you cannot be "critically behind" on something you
|
|
79
|
+
* already patched. Defaults to true so callers without version data keep
|
|
80
|
+
* the conservative grade.
|
|
81
|
+
*/
|
|
82
|
+
stillVulnerable?: boolean,
|
|
83
|
+
/**
|
|
84
|
+
* The installed version(s), when known. A registry row is only a version
|
|
85
|
+
* UPDATE when it is newer than an install (AD-041): the row for the version
|
|
86
|
+
* every project already runs is not missed intelligence. Unknown keeps the
|
|
87
|
+
* conservative grade.
|
|
88
|
+
*/
|
|
89
|
+
installedVersion?: string | readonly string[] | null,
|
|
90
|
+
/** The tier of the most severe stored advisory reaching an install; null when ungraded. */
|
|
91
|
+
advisoryTier?: AdvisoryTier | null): GapSeverity;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Knowledge-gap grading: the pure rules that decide whether an unread item is
|
|
4
|
+
* evidence about a dependency and how severe the gap is. The database walk
|
|
5
|
+
* lives in knowledge-gaps.ts; advisory ranges in knowledge-gap-ranges.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { comparePrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
8
|
+
// Word-boundary matching prevents "cve" matching inside "achieve", "receiver", etc.
|
|
9
|
+
function hasWordBoundary(text, term) {
|
|
10
|
+
const regex = new RegExp(`\\b${escapeRegExp(term)}\\b`, "i");
|
|
11
|
+
return regex.test(text);
|
|
12
|
+
}
|
|
13
|
+
// Package names can contain regex metacharacters (@scope/name, c++, next.js).
|
|
14
|
+
function escapeRegExp(s) {
|
|
15
|
+
return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
16
|
+
}
|
|
17
|
+
// True when `text` mentions the package name as a whole word. Word characters
|
|
18
|
+
// for this purpose include - and _ (so dep "hono" does NOT match "hono-shim",
|
|
19
|
+
// and never matches "in HONOr of"). @scope/name matches as the full literal.
|
|
20
|
+
export function mentionsPackage(text, pkg) {
|
|
21
|
+
const regex = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(pkg)}($|[^A-Za-z0-9_-])`, "i");
|
|
22
|
+
return regex.test(text);
|
|
23
|
+
}
|
|
24
|
+
/** Package-registry sources: every row is a published version of a package. */
|
|
25
|
+
export const REGISTRY_SOURCES = new Set(["crates_io", "npm_registry", "pypi", "go_modules"]);
|
|
26
|
+
/** Advisory sources: every row is a security advisory. */
|
|
27
|
+
export const ADVISORY_SOURCES = new Set(["osv", "cve"]);
|
|
28
|
+
/**
|
|
29
|
+
* A registry advisory row (`knowledge_decay::is_advisory_row`). An editorial
|
|
30
|
+
* story the pipeline typed `security_advisory` is not one: it cites by title
|
|
31
|
+
* like any other story and is never proof of exposure (AD-040 rule 4).
|
|
32
|
+
*/
|
|
33
|
+
export function isAdvisoryRow(item) {
|
|
34
|
+
return ADVISORY_SOURCES.has(item.source_type ?? "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The subject package of an advisory title in the ingested "[ID] package:
|
|
38
|
+
* summary" shape ("[GHSA-h395-gr6q-cpjc] jsonwebtoken: ..."), or null when
|
|
39
|
+
* the title has another shape and cannot be read that way.
|
|
40
|
+
*/
|
|
41
|
+
export function advisorySubject(title) {
|
|
42
|
+
const match = /^\s*\[[^\]]+\]\s*([^:\s]+)\s*:/.exec(title);
|
|
43
|
+
return match ? match[1] : null;
|
|
44
|
+
}
|
|
45
|
+
/** Same package name, ignoring case and the crates.io `-`/`_` split. */
|
|
46
|
+
export function samePackage(a, b) {
|
|
47
|
+
const norm = (s) => s.toLowerCase().replace(/_/g, "-");
|
|
48
|
+
return norm(a) === norm(b);
|
|
49
|
+
}
|
|
50
|
+
/** The released version a registry row announces ("npm: react v19.3.0" → "19.3.0"), if any. */
|
|
51
|
+
export function registryVersionFromTitle(title) {
|
|
52
|
+
const versions = title.match(/\bv?\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?\b/g);
|
|
53
|
+
if (!versions || versions.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
return versions[versions.length - 1].replace(/^v/, "");
|
|
56
|
+
}
|
|
57
|
+
/** Parse a source_items.published_at value (SQLite "YYYY-MM-DD HH:MM:SS" or ISO). */
|
|
58
|
+
export function parsePublishedAt(value) {
|
|
59
|
+
if (!value)
|
|
60
|
+
return null;
|
|
61
|
+
const iso = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
|
|
62
|
+
const ms = Date.parse(iso);
|
|
63
|
+
return Number.isNaN(ms) ? null : ms;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Does `item` cite `packageName` by its title alone? An advisory row cites
|
|
67
|
+
* only its SUBJECT package and never falls back to a word in its title
|
|
68
|
+
* ("[CVE-2026-63642] MagicMirror newsfeed Socket.IO notification ..." is not
|
|
69
|
+
* a socket.io advisory); every other row names the package in its title.
|
|
70
|
+
*/
|
|
71
|
+
function citesByTitle(item, packageName) {
|
|
72
|
+
const title = item.title || "";
|
|
73
|
+
if (isAdvisoryRow(item)) {
|
|
74
|
+
const subject = advisorySubject(title);
|
|
75
|
+
return subject !== null && samePackage(subject, packageName);
|
|
76
|
+
}
|
|
77
|
+
return mentionsPackage(title, packageName);
|
|
78
|
+
}
|
|
79
|
+
const RETIREMENT_KEYWORDS = ["breaking", "deprecated", "deprecation", "eol"];
|
|
80
|
+
const CONSEQUENCE_KEYWORDS = ["release", "released", "update", "upgrade"];
|
|
81
|
+
/**
|
|
82
|
+
* Grade a knowledge gap by CONSEQUENCE, never by volume, on the tiers the
|
|
83
|
+
* desktop app draws (`knowledge_decay::classify_severity`):
|
|
84
|
+
*
|
|
85
|
+
* - `critical`: an advisory citing the dependency still reaches an install,
|
|
86
|
+
* and the most severe advisory reaching an install is critical or high by
|
|
87
|
+
* its OWN grade — CVSS band, else the source's curated label (AD-040 rule 2).
|
|
88
|
+
* - `high`: the same with a medium, low or ungraded advisory; or a title
|
|
89
|
+
* naming the dependency with a breaking change, deprecation or end of life.
|
|
90
|
+
* - `medium`: a registry release newer than an install, or a title naming the
|
|
91
|
+
* dependency with a release, update or upgrade.
|
|
92
|
+
* - `low`: everything else, including a large pile of passing mentions.
|
|
93
|
+
*
|
|
94
|
+
* `critical` used to mean "an advisory names the dependency", whatever the
|
|
95
|
+
* advisory's own tier: the jsonwebtoken crate's GHSA-h395-gr6q-cpjc, graded
|
|
96
|
+
* medium by its source and High by the app, was a critical gap here (measured
|
|
97
|
+
* 2026-09-11). Earlier still, `medium` meant "3+ recent unread mentions"
|
|
98
|
+
* matched on the content body: a `tracing` gap evidenced by "The Matrix:
|
|
99
|
+
* Writing Code That Doesn't Need Comments", a `typescript` gap by a
|
|
100
|
+
* Databricks job posting, fourteen of fifteen gaps noise. Two implementations
|
|
101
|
+
* of one concept disagreeing is what this mirrors the app to avoid.
|
|
102
|
+
*/
|
|
103
|
+
export function gradeGap(items, packageName,
|
|
104
|
+
/**
|
|
105
|
+
* An advisory citing this dependency still reaches an install, or cannot be
|
|
106
|
+
* ruled out. False when every such advisory is fixed at or below the
|
|
107
|
+
* installed versions: you cannot be "critically behind" on something you
|
|
108
|
+
* already patched. Defaults to true so callers without version data keep
|
|
109
|
+
* the conservative grade.
|
|
110
|
+
*/
|
|
111
|
+
stillVulnerable = true,
|
|
112
|
+
/**
|
|
113
|
+
* The installed version(s), when known. A registry row is only a version
|
|
114
|
+
* UPDATE when it is newer than an install (AD-041): the row for the version
|
|
115
|
+
* every project already runs is not missed intelligence. Unknown keeps the
|
|
116
|
+
* conservative grade.
|
|
117
|
+
*/
|
|
118
|
+
installedVersion = null,
|
|
119
|
+
/** The tier of the most severe stored advisory reaching an install; null when ungraded. */
|
|
120
|
+
advisoryTier = null) {
|
|
121
|
+
const cites = (item) => item.cites ?? citesByTitle(item, packageName);
|
|
122
|
+
// The tiers below the security tier read the TITLE, as the app's do: it
|
|
123
|
+
// must name the dependency (an advisory's subject counts).
|
|
124
|
+
const titleNames = (item) => mentionsPackage(item.title || "", packageName) || (isAdvisoryRow(item) && citesByTitle(item, packageName));
|
|
125
|
+
// "Announcing <thing> <version>" is the canonical release phrasing and names
|
|
126
|
+
// no other keyword. The version token is REQUIRED, matching the rule
|
|
127
|
+
// `content_dna_classifiers` settled on: it keeps "Announcing axum 0.8.0" and
|
|
128
|
+
// rejects "Announcing Toasty, an async ORM" and "Announcing our Series B".
|
|
129
|
+
const announcesAVersion = (title) => (hasWordBoundary(title, "announcing") || hasWordBoundary(title, "introducing")) && /\bv?\d+\.\d+/.test(title);
|
|
130
|
+
const retires = (title) => RETIREMENT_KEYWORDS.some((kw) => hasWordBoundary(title, kw)) || /\bend[\s-]of[\s-]life\b/i.test(title);
|
|
131
|
+
const carriesConsequence = (title) => CONSEQUENCE_KEYWORDS.some((kw) => hasWordBoundary(title, kw)) || announcesAVersion(title);
|
|
132
|
+
// A registry row IS a version update: "crates.io: serde v1.0.220" names no
|
|
133
|
+
// consequence keyword, but an unread release of a direct dependency is the
|
|
134
|
+
// exact thing `knowledge_decay::gap_is_substantive` counts — provided it is
|
|
135
|
+
// newer than an install (live: "npm: @tauri-apps/api v2.11.1" on 2.11.1).
|
|
136
|
+
// Compared by semver precedence, so 0.10.0-rc.19 is newer than rc.18.
|
|
137
|
+
const installed = (typeof installedVersion === "string" ? [installedVersion] : (installedVersion ?? []))
|
|
138
|
+
.map((v) => parseSemverPrecedence(v))
|
|
139
|
+
.filter((v) => v !== null);
|
|
140
|
+
const isNewerRelease = (item) => {
|
|
141
|
+
if (!REGISTRY_SOURCES.has(item.source_type ?? ""))
|
|
142
|
+
return false;
|
|
143
|
+
const released = parseSemverPrecedence(registryVersionFromTitle(item.title || "") ?? "");
|
|
144
|
+
if (released === null || installed.length === 0)
|
|
145
|
+
return true;
|
|
146
|
+
return installed.some((v) => comparePrecedence(released, v) > 0);
|
|
147
|
+
};
|
|
148
|
+
if (stillVulnerable && items.some((item) => isAdvisoryRow(item) && cites(item))) {
|
|
149
|
+
return advisoryTier === "critical" || advisoryTier === "high" ? "critical" : "high";
|
|
150
|
+
}
|
|
151
|
+
if (items.some((item) => cites(item) && titleNames(item) && retires(item.title || "")))
|
|
152
|
+
return "high";
|
|
153
|
+
if (items.some((item) => cites(item) && (isNewerRelease(item) || (titleNames(item) && carriesConsequence(item.title || ""))))) {
|
|
154
|
+
return "medium";
|
|
155
|
+
}
|
|
156
|
+
return "low";
|
|
157
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge-gap installs: the install each declaring project carries, with
|
|
3
|
+
* its ecosystem (AD-045: a package is (ecosystem, name)). The MCP twin of the
|
|
4
|
+
* version join in `temporal::get_all_dependencies` (tiers i and ii),
|
|
5
|
+
* `knowledge_decay::installs_for` and `osv::exposure::canonical`.
|
|
6
|
+
*
|
|
7
|
+
* The first cut resolved ONE version per package name (first seen across the
|
|
8
|
+
* machine) and displayed it beside whichever project row came first, so a gap
|
|
9
|
+
* could judge one project's install and name another's.
|
|
10
|
+
*/
|
|
11
|
+
import type { FourDADatabase } from "../db.js";
|
|
12
|
+
import type { LiveIntelligence } from "../live/index.js";
|
|
13
|
+
/** A `project_dependencies` row as the gap walk reads it. */
|
|
14
|
+
export interface DeclaringRow {
|
|
15
|
+
package_name: string;
|
|
16
|
+
version: string | null;
|
|
17
|
+
project_path: string;
|
|
18
|
+
language: string;
|
|
19
|
+
}
|
|
20
|
+
/** One declaring project's install of a dependency. */
|
|
21
|
+
export interface Install {
|
|
22
|
+
projectPath: string;
|
|
23
|
+
language: string;
|
|
24
|
+
/** The OSV ecosystem ("npm", "crates.io", ...); null when the label names none the app knows. */
|
|
25
|
+
ecosystem: string | null;
|
|
26
|
+
version: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The OSV ecosystem a `project_dependencies.language` or
|
|
30
|
+
* `user_dependencies.ecosystem` label names, or null when it names none
|
|
31
|
+
* (`osv::exposure::canonical`). `mapEcosystem` falls back to npm for a label
|
|
32
|
+
* it does not know; that fallback must not decide which advisories apply.
|
|
33
|
+
*/
|
|
34
|
+
export declare function osvEcosystem(label: string | null | undefined): string | null;
|
|
35
|
+
export declare const sameEcosystem: (a: string, b: string) => boolean;
|
|
36
|
+
/** Does a stored name denote this dependency? crates.io treats `-` and `_` as one namespace. */
|
|
37
|
+
export declare function denotes(storedName: string, ecosystem: string | null, depName: string): boolean;
|
|
38
|
+
/** The other crates.io spelling of a name (`http_body_util` / `http-body-util`). */
|
|
39
|
+
export declare const otherSpelling: (name: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the install each declaring project carries: the manifest's own
|
|
42
|
+
* version when it is a real version; else the lockfile-resolved one in
|
|
43
|
+
* `user_dependencies` (the same project and ecosystem family, then a lockfile
|
|
44
|
+
* under the same active repo root — a workspace member resolved by the
|
|
45
|
+
* workspace lockfile); else the live resolver's version for that project.
|
|
46
|
+
* Never across repo roots or ecosystems: a project is never judged on a
|
|
47
|
+
* sibling's version.
|
|
48
|
+
*/
|
|
49
|
+
export declare class InstallResolver {
|
|
50
|
+
private readonly lockStmt;
|
|
51
|
+
private readonly lockRowsByName;
|
|
52
|
+
private readonly resolved;
|
|
53
|
+
private readonly activeRoots;
|
|
54
|
+
constructor(db: FourDADatabase, liveIntel: Pick<LiveIntelligence, "getResolvedDeps" | "isInitialized"> | null | undefined, activeRoots: readonly string[]);
|
|
55
|
+
/** One install per declaring project and ecosystem. */
|
|
56
|
+
installsFor(rows: DeclaringRow[]): Install[];
|
|
57
|
+
private versionOf;
|
|
58
|
+
private lockRows;
|
|
59
|
+
/**
|
|
60
|
+
* The live resolver's version for THIS project: the resolution whose
|
|
61
|
+
* manifest directory is the project. Without a lockfile table (standalone
|
|
62
|
+
* mode, one project) the resolver's version for the package is the install.
|
|
63
|
+
*/
|
|
64
|
+
private fromResolver;
|
|
65
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Knowledge-gap installs: the install each declaring project carries, with
|
|
4
|
+
* its ecosystem (AD-045: a package is (ecosystem, name)). The MCP twin of the
|
|
5
|
+
* version join in `temporal::get_all_dependencies` (tiers i and ii),
|
|
6
|
+
* `knowledge_decay::installs_for` and `osv::exposure::canonical`.
|
|
7
|
+
*
|
|
8
|
+
* The first cut resolved ONE version per package name (first seen across the
|
|
9
|
+
* machine) and displayed it beside whichever project row came first, so a gap
|
|
10
|
+
* could judge one project's install and name another's.
|
|
11
|
+
*/
|
|
12
|
+
import { parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
13
|
+
import { mapEcosystem } from "../live/version-resolver.js";
|
|
14
|
+
import { samePackage } from "./knowledge-gap-grading.js";
|
|
15
|
+
import { allRows, normPath, sharesRoot } from "./knowledge-gap-scope.js";
|
|
16
|
+
const NPM_LABELS = new Set(["npm", "javascript", "typescript", "node", "js", "ts"]);
|
|
17
|
+
/**
|
|
18
|
+
* The OSV ecosystem a `project_dependencies.language` or
|
|
19
|
+
* `user_dependencies.ecosystem` label names, or null when it names none
|
|
20
|
+
* (`osv::exposure::canonical`). `mapEcosystem` falls back to npm for a label
|
|
21
|
+
* it does not know; that fallback must not decide which advisories apply.
|
|
22
|
+
*/
|
|
23
|
+
export function osvEcosystem(label) {
|
|
24
|
+
const lower = (label ?? "").trim().toLowerCase();
|
|
25
|
+
const mapped = mapEcosystem(lower);
|
|
26
|
+
return mapped === "npm" && !NPM_LABELS.has(lower) ? null : mapped;
|
|
27
|
+
}
|
|
28
|
+
/** The family a lockfile join compares: an unknown label is its own family (`temporal::ecosystem_family`). */
|
|
29
|
+
function ecosystemFamily(label) {
|
|
30
|
+
return osvEcosystem(label) ?? `other:${(label ?? "").trim().toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
export const sameEcosystem = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
33
|
+
/** Does a stored name denote this dependency? crates.io treats `-` and `_` as one namespace. */
|
|
34
|
+
export function denotes(storedName, ecosystem, depName) {
|
|
35
|
+
if (storedName.toLowerCase() === depName.toLowerCase())
|
|
36
|
+
return true;
|
|
37
|
+
return ecosystem !== null && sameEcosystem(ecosystem, "crates.io") && samePackage(storedName, depName);
|
|
38
|
+
}
|
|
39
|
+
/** The other crates.io spelling of a name (`http_body_util` / `http-body-util`). */
|
|
40
|
+
export const otherSpelling = (name) => name.includes("_") ? name.replace(/_/g, "-") : name.replace(/-/g, "_");
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the install each declaring project carries: the manifest's own
|
|
43
|
+
* version when it is a real version; else the lockfile-resolved one in
|
|
44
|
+
* `user_dependencies` (the same project and ecosystem family, then a lockfile
|
|
45
|
+
* under the same active repo root — a workspace member resolved by the
|
|
46
|
+
* workspace lockfile); else the live resolver's version for that project.
|
|
47
|
+
* Never across repo roots or ecosystems: a project is never judged on a
|
|
48
|
+
* sibling's version.
|
|
49
|
+
*/
|
|
50
|
+
export class InstallResolver {
|
|
51
|
+
lockStmt = null;
|
|
52
|
+
lockRowsByName = new Map();
|
|
53
|
+
resolved;
|
|
54
|
+
activeRoots;
|
|
55
|
+
constructor(db, liveIntel, activeRoots) {
|
|
56
|
+
this.activeRoots = activeRoots;
|
|
57
|
+
this.resolved = liveIntel && liveIntel.isInitialized() ? liveIntel.getResolvedDeps() : [];
|
|
58
|
+
const has = (column) => db.hasColumn("user_dependencies", column);
|
|
59
|
+
if (!["project_path", "package_name", "version", "ecosystem"].every(has))
|
|
60
|
+
return;
|
|
61
|
+
const order = [has("is_direct") ? "is_direct DESC" : "", has("last_seen_at") ? "last_seen_at DESC" : ""]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(", ");
|
|
64
|
+
try {
|
|
65
|
+
this.lockStmt = db.getRawDb().prepare(`SELECT project_path, package_name, version, ecosystem FROM user_dependencies
|
|
66
|
+
WHERE lower(package_name) IN (lower(?), lower(?)) AND version IS NOT NULL AND version != ''
|
|
67
|
+
${order ? `ORDER BY ${order}` : ""}`);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No readable lockfile table: fall through to the resolver.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** One install per declaring project and ecosystem. */
|
|
74
|
+
installsFor(rows) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const installs = [];
|
|
77
|
+
for (const row of rows) {
|
|
78
|
+
const key = `${normPath(row.project_path)}\0${ecosystemFamily(row.language)}`;
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
installs.push({
|
|
83
|
+
projectPath: row.project_path,
|
|
84
|
+
language: row.language,
|
|
85
|
+
ecosystem: osvEcosystem(row.language),
|
|
86
|
+
version: this.versionOf(row),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return installs;
|
|
90
|
+
}
|
|
91
|
+
versionOf(row) {
|
|
92
|
+
// A manifest records a RANGE when it records anything ("^4.1.5"); only a
|
|
93
|
+
// real version is an install.
|
|
94
|
+
if (row.version && parseSemverPrecedence(row.version) !== null)
|
|
95
|
+
return row.version;
|
|
96
|
+
const family = ecosystemFamily(row.language);
|
|
97
|
+
const locked = this.lockRows(row.package_name).filter((l) => ecosystemFamily(l.ecosystem) === family && denotes(l.package_name, family, row.package_name));
|
|
98
|
+
const here = normPath(row.project_path);
|
|
99
|
+
const own = locked.find((l) => normPath(l.project_path) === here);
|
|
100
|
+
if (own)
|
|
101
|
+
return own.version;
|
|
102
|
+
const workspace = locked.find((l) => sharesRoot(row.project_path, l.project_path, this.activeRoots));
|
|
103
|
+
if (workspace)
|
|
104
|
+
return workspace.version;
|
|
105
|
+
return this.fromResolver(row);
|
|
106
|
+
}
|
|
107
|
+
lockRows(name) {
|
|
108
|
+
if (!this.lockStmt)
|
|
109
|
+
return [];
|
|
110
|
+
const key = name.toLowerCase();
|
|
111
|
+
let rows = this.lockRowsByName.get(key);
|
|
112
|
+
if (!rows) {
|
|
113
|
+
rows = allRows(this.lockStmt, name, otherSpelling(name));
|
|
114
|
+
this.lockRowsByName.set(key, rows);
|
|
115
|
+
}
|
|
116
|
+
return rows;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The live resolver's version for THIS project: the resolution whose
|
|
120
|
+
* manifest directory is the project. Without a lockfile table (standalone
|
|
121
|
+
* mode, one project) the resolver's version for the package is the install.
|
|
122
|
+
*/
|
|
123
|
+
fromResolver(row) {
|
|
124
|
+
const ecosystem = osvEcosystem(row.language);
|
|
125
|
+
if (ecosystem === null)
|
|
126
|
+
return null;
|
|
127
|
+
const matches = this.resolved.filter((d) => d.version && sameEcosystem(d.ecosystem, ecosystem) && denotes(d.name, ecosystem, row.package_name));
|
|
128
|
+
const here = normPath(row.project_path);
|
|
129
|
+
const own = matches.find((d) => (d.sourceDirs ?? []).some((dir) => normPath(dir) === here));
|
|
130
|
+
if (own)
|
|
131
|
+
return own.version;
|
|
132
|
+
return this.lockStmt === null ? (matches[0]?.version ?? null) : null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSV affected-range evaluation for knowledge gaps: the TypeScript twin of
|
|
3
|
+
* `src-tauri/src/osv/matching.rs::check_version_affected`, so the desktop app
|
|
4
|
+
* and this server answer "is this install inside that advisory?" the same way.
|
|
5
|
+
*
|
|
6
|
+
* The first version here understood only `introduced`/`fixed`. GHSA-9crc-q9x8-hgqq
|
|
7
|
+
* stores vitest's pre-1.0 window as `[{"introduced":"0"},{"last_affected":"0.0.125"}]`;
|
|
8
|
+
* the loop never saw a `fixed`, read the trailing `introduced: "0"` as
|
|
9
|
+
* "affected from 0 onward", and put EVERY vitest version inside a CVSS 9.6
|
|
10
|
+
* advisory (measured 2026-09-11 against vitest 4.1.11, whose one real exposure
|
|
11
|
+
* is fixed at exactly 4.1.11).
|
|
12
|
+
*/
|
|
13
|
+
/** One event of an OSV affected range; OSV sets exactly one key per event. */
|
|
14
|
+
export interface AdvisoryRangeEvent {
|
|
15
|
+
introduced?: string;
|
|
16
|
+
fixed?: string;
|
|
17
|
+
last_affected?: string;
|
|
18
|
+
limit?: string;
|
|
19
|
+
}
|
|
20
|
+
/** An advisory's own severity tier. */
|
|
21
|
+
export type AdvisoryTier = "critical" | "high" | "medium" | "low";
|
|
22
|
+
/**
|
|
23
|
+
* The SEMVER/ECOSYSTEM ranges of a stored `osv_advisories.affected_ranges`
|
|
24
|
+
* value as event lists, or null when the value is missing or unreadable.
|
|
25
|
+
* `[]` is readable and contains nothing. GIT ranges carry commit hashes and
|
|
26
|
+
* are skipped, as the Rust reference skips them; a range with no `type` is
|
|
27
|
+
* read as SEMVER.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseAffectedRanges(json: string | null | undefined): AdvisoryRangeEvent[][] | null;
|
|
30
|
+
/** Is `installed` inside any of `ranges`? Null when the installed version is unreadable. */
|
|
31
|
+
export declare function rangesContain(ranges: AdvisoryRangeEvent[][], installed: string): boolean | null;
|
|
32
|
+
/**
|
|
33
|
+
* Is `installed` inside any of these advisory ranges?
|
|
34
|
+
*
|
|
35
|
+
* An advisory naming your dependency is only a gap if you are actually
|
|
36
|
+
* exposed: the live tool once graded three Hono CVEs `critical` on hono
|
|
37
|
+
* 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
|
|
38
|
+
* pinned past via `pnpm.overrides`.
|
|
39
|
+
*
|
|
40
|
+
* Conservative by construction: a missing or unreadable installed version
|
|
41
|
+
* counts as AFFECTED. Never claim someone is safe on missing information.
|
|
42
|
+
*/
|
|
43
|
+
export declare function versionInAnyRange(ranges: AdvisoryRangeEvent[][], installed: string | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* An advisory's tier: its CVSS band first (>= 9 critical, >= 7 high, >= 4
|
|
46
|
+
* medium, else low: `osv::types::cvss_band`), else the source's curated label,
|
|
47
|
+
* else null (ungraded). Mirrors `knowledge_decay::advisory_tier_for`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function advisoryTier(cvssScore: number | null | undefined, label: string | null | undefined): AdvisoryTier | null;
|
|
50
|
+
/** The more severe of two tiers; null (ungraded) ranks below every tier. */
|
|
51
|
+
export declare function moreSevereTier(a: AdvisoryTier | null, b: AdvisoryTier | null): AdvisoryTier | null;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* OSV affected-range evaluation for knowledge gaps: the TypeScript twin of
|
|
4
|
+
* `src-tauri/src/osv/matching.rs::check_version_affected`, so the desktop app
|
|
5
|
+
* and this server answer "is this install inside that advisory?" the same way.
|
|
6
|
+
*
|
|
7
|
+
* The first version here understood only `introduced`/`fixed`. GHSA-9crc-q9x8-hgqq
|
|
8
|
+
* stores vitest's pre-1.0 window as `[{"introduced":"0"},{"last_affected":"0.0.125"}]`;
|
|
9
|
+
* the loop never saw a `fixed`, read the trailing `introduced: "0"` as
|
|
10
|
+
* "affected from 0 onward", and put EVERY vitest version inside a CVSS 9.6
|
|
11
|
+
* advisory (measured 2026-09-11 against vitest 4.1.11, whose one real exposure
|
|
12
|
+
* is fixed at exactly 4.1.11).
|
|
13
|
+
*/
|
|
14
|
+
import { comparePrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
15
|
+
/**
|
|
16
|
+
* The SEMVER/ECOSYSTEM ranges of a stored `osv_advisories.affected_ranges`
|
|
17
|
+
* value as event lists, or null when the value is missing or unreadable.
|
|
18
|
+
* `[]` is readable and contains nothing. GIT ranges carry commit hashes and
|
|
19
|
+
* are skipped, as the Rust reference skips them; a range with no `type` is
|
|
20
|
+
* read as SEMVER.
|
|
21
|
+
*/
|
|
22
|
+
export function parseAffectedRanges(json) {
|
|
23
|
+
if (typeof json !== "string" || json.trim() === "")
|
|
24
|
+
return null;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(json);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(parsed))
|
|
33
|
+
return null;
|
|
34
|
+
const ranges = [];
|
|
35
|
+
for (const range of parsed) {
|
|
36
|
+
if (typeof range !== "object" || range === null)
|
|
37
|
+
continue;
|
|
38
|
+
const { type, events } = range;
|
|
39
|
+
if (typeof type === "string" && type !== "SEMVER" && type !== "ECOSYSTEM")
|
|
40
|
+
continue;
|
|
41
|
+
if (!Array.isArray(events))
|
|
42
|
+
continue;
|
|
43
|
+
ranges.push(events.filter((e) => typeof e === "object" && e !== null));
|
|
44
|
+
}
|
|
45
|
+
return ranges;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* OSV's marker for a boundary nobody knows ("2.5.0-NA", from PYSEC imports).
|
|
49
|
+
* `matching.rs::is_unknown_bound`: such a bound grounds no match.
|
|
50
|
+
*/
|
|
51
|
+
function isUnknownBound(v) {
|
|
52
|
+
const t = v.trim();
|
|
53
|
+
return t.endsWith("-NA") || t.endsWith("-na");
|
|
54
|
+
}
|
|
55
|
+
function closingEvent(event) {
|
|
56
|
+
const read = (v) => (isUnknownBound(v) ? null : parseSemverPrecedence(v));
|
|
57
|
+
if (typeof event.fixed === "string")
|
|
58
|
+
return { bound: read(event.fixed), inclusive: false };
|
|
59
|
+
if (typeof event.last_affected === "string")
|
|
60
|
+
return { bound: read(event.last_affected), inclusive: true };
|
|
61
|
+
if (typeof event.limit === "string") {
|
|
62
|
+
return { bound: event.limit.trim() === "*" ? "unbounded" : read(event.limit), inclusive: false };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Is `version` inside this one range? Events pair up in order: `introduced`
|
|
68
|
+
* opens, `fixed` and `limit` close exclusively, `last_affected` closes
|
|
69
|
+
* inclusively, and one range may hold several pairs. A closing event always
|
|
70
|
+
* ends the pair it closes, even when its bound is unknown or unreadable: such
|
|
71
|
+
* a bound matches nothing and leaves no open range behind. Only an
|
|
72
|
+
* `introduced` that nothing closed means "affected from there on".
|
|
73
|
+
*/
|
|
74
|
+
function eventsContain(events, version) {
|
|
75
|
+
const atOrAbove = (lower) => lower === "zero" || comparePrecedence(version, lower) >= 0;
|
|
76
|
+
let lower = null;
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
if (typeof event.introduced === "string") {
|
|
79
|
+
lower = event.introduced.trim() === "0" ? "zero" : parseSemverPrecedence(event.introduced);
|
|
80
|
+
}
|
|
81
|
+
const upper = closingEvent(event);
|
|
82
|
+
if (upper === null)
|
|
83
|
+
continue;
|
|
84
|
+
if (lower !== null && upper.bound !== null && atOrAbove(lower)) {
|
|
85
|
+
if (upper.bound === "unbounded")
|
|
86
|
+
return true;
|
|
87
|
+
const cmp = comparePrecedence(version, upper.bound);
|
|
88
|
+
if (upper.inclusive ? cmp <= 0 : cmp < 0)
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
lower = null;
|
|
92
|
+
}
|
|
93
|
+
return lower !== null && atOrAbove(lower);
|
|
94
|
+
}
|
|
95
|
+
/** Is `installed` inside any of `ranges`? Null when the installed version is unreadable. */
|
|
96
|
+
export function rangesContain(ranges, installed) {
|
|
97
|
+
const version = parseSemverPrecedence(installed);
|
|
98
|
+
if (version === null)
|
|
99
|
+
return null;
|
|
100
|
+
return ranges.some((events) => eventsContain(events, version));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Is `installed` inside any of these advisory ranges?
|
|
104
|
+
*
|
|
105
|
+
* An advisory naming your dependency is only a gap if you are actually
|
|
106
|
+
* exposed: the live tool once graded three Hono CVEs `critical` on hono
|
|
107
|
+
* 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
|
|
108
|
+
* pinned past via `pnpm.overrides`.
|
|
109
|
+
*
|
|
110
|
+
* Conservative by construction: a missing or unreadable installed version
|
|
111
|
+
* counts as AFFECTED. Never claim someone is safe on missing information.
|
|
112
|
+
*/
|
|
113
|
+
export function versionInAnyRange(ranges, installed) {
|
|
114
|
+
if (!installed)
|
|
115
|
+
return true;
|
|
116
|
+
return rangesContain(ranges, installed) ?? true;
|
|
117
|
+
}
|
|
118
|
+
const TIERS = new Set(["critical", "high", "medium", "low"]);
|
|
119
|
+
const isTier = (s) => TIERS.has(s);
|
|
120
|
+
/**
|
|
121
|
+
* An advisory's tier: its CVSS band first (>= 9 critical, >= 7 high, >= 4
|
|
122
|
+
* medium, else low: `osv::types::cvss_band`), else the source's curated label,
|
|
123
|
+
* else null (ungraded). Mirrors `knowledge_decay::advisory_tier_for`.
|
|
124
|
+
*/
|
|
125
|
+
export function advisoryTier(cvssScore, label) {
|
|
126
|
+
if (typeof cvssScore === "number" && Number.isFinite(cvssScore)) {
|
|
127
|
+
if (cvssScore >= 9)
|
|
128
|
+
return "critical";
|
|
129
|
+
if (cvssScore >= 7)
|
|
130
|
+
return "high";
|
|
131
|
+
if (cvssScore >= 4)
|
|
132
|
+
return "medium";
|
|
133
|
+
return "low";
|
|
134
|
+
}
|
|
135
|
+
const l = typeof label === "string" ? label.trim().toLowerCase() : "";
|
|
136
|
+
return isTier(l) ? l : null;
|
|
137
|
+
}
|
|
138
|
+
const TIER_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
139
|
+
/** The more severe of two tiers; null (ungraded) ranks below every tier. */
|
|
140
|
+
export function moreSevereTier(a, b) {
|
|
141
|
+
if (a === null)
|
|
142
|
+
return b;
|
|
143
|
+
if (b === null)
|
|
144
|
+
return a;
|
|
145
|
+
return TIER_RANK[b] > TIER_RANK[a] ? b : a;
|
|
146
|
+
}
|