@4da/mcp-server 6.2.3 → 6.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +520 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +289 -16
- package/dist/live/fix-paths.js +876 -93
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/range-satisfies.d.ts +4 -2
- package/dist/live/range-satisfies.js +7 -5
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/refresh-rule.d.ts +36 -0
- package/dist/live/refresh-rule.js +49 -0
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +51 -0
- package/dist/live/version-compare.js +152 -1
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +70 -7
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby (Gemfile.lock -> RubyGems) and PHP (composer.lock -> Packagist).
|
|
3
|
+
*
|
|
4
|
+
* Until 6.2.5 neither was read: a Rails or Symfony project came back "No
|
|
5
|
+
* manifest files found... run from your project root", which reads as the
|
|
6
|
+
* user's mistake rather than an ecosystem this server did not cover (audit
|
|
7
|
+
* A2, 2026-10-12). Both readers mirror the desktop app's
|
|
8
|
+
* (src-tauri/src/ace/lockfile/ruby.rs; ProjectScanner::parse_composer_lock),
|
|
9
|
+
* and the conformance corpus scores them against the same truth
|
|
10
|
+
* (rb-js-rails-6.0, php-js-symfony-demo).
|
|
11
|
+
*/
|
|
12
|
+
import { InstanceSet, type VersionSource } from "./lockfile-types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Gemfile.lock: the `specs:` of each `GEM` section (registry gems). GIT and
|
|
15
|
+
* PATH sections are local or git sources, not RubyGems releases, and are
|
|
16
|
+
* skipped. A platform suffix (`nokogiri (1.10.4-x86_64-linux)`) is not part
|
|
17
|
+
* of the version: RubyGems prereleases use `.pre`, never `-`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readGemfileLock(content: string): {
|
|
20
|
+
instances: InstanceSet;
|
|
21
|
+
versions: Map<string, string>;
|
|
22
|
+
direct: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* composer.lock: `packages` (runtime) and `packages-dev` (dev). A leading `v`
|
|
26
|
+
* is dropped (`v5.4.0` is release 5.4.0 on Packagist and in OSV). A branch
|
|
27
|
+
* checkout (`dev-main`) names no version and is skipped; a numbered branch
|
|
28
|
+
* alias (`3.0.x-dev`) is kept, as osv-scanner and the desktop app keep it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readComposerLock(lock: {
|
|
31
|
+
packages?: unknown;
|
|
32
|
+
"packages-dev"?: unknown;
|
|
33
|
+
}): {
|
|
34
|
+
instances: InstanceSet;
|
|
35
|
+
versions: Map<string, string>;
|
|
36
|
+
};
|
|
37
|
+
export declare function resolveRuby(cwd: string): VersionSource;
|
|
38
|
+
export declare function resolvePhp(cwd: string): VersionSource;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Ruby (Gemfile.lock -> RubyGems) and PHP (composer.lock -> Packagist).
|
|
4
|
+
*
|
|
5
|
+
* Until 6.2.5 neither was read: a Rails or Symfony project came back "No
|
|
6
|
+
* manifest files found... run from your project root", which reads as the
|
|
7
|
+
* user's mistake rather than an ecosystem this server did not cover (audit
|
|
8
|
+
* A2, 2026-10-12). Both readers mirror the desktop app's
|
|
9
|
+
* (src-tauri/src/ace/lockfile/ruby.rs; ProjectScanner::parse_composer_lock),
|
|
10
|
+
* and the conformance corpus scores them against the same truth
|
|
11
|
+
* (rb-js-rails-6.0, php-js-symfony-demo).
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { emptySource, found, InstanceSet } from "./lockfile-types.js";
|
|
16
|
+
/** Lines longer than this are not lockfile syntax; skipping them keeps every pattern below linear. */
|
|
17
|
+
const MAX_LINE = 4096;
|
|
18
|
+
/**
|
|
19
|
+
* Gemfile.lock: the `specs:` of each `GEM` section (registry gems). GIT and
|
|
20
|
+
* PATH sections are local or git sources, not RubyGems releases, and are
|
|
21
|
+
* skipped. A platform suffix (`nokogiri (1.10.4-x86_64-linux)`) is not part
|
|
22
|
+
* of the version: RubyGems prereleases use `.pre`, never `-`.
|
|
23
|
+
*/
|
|
24
|
+
export function readGemfileLock(content) {
|
|
25
|
+
const instances = new InstanceSet();
|
|
26
|
+
const versions = new Map();
|
|
27
|
+
const direct = [];
|
|
28
|
+
let section = "";
|
|
29
|
+
let inSpecs = false;
|
|
30
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
31
|
+
if (raw.length > MAX_LINE || raw.trim() === "")
|
|
32
|
+
continue;
|
|
33
|
+
if (!raw.startsWith(" ")) {
|
|
34
|
+
section = raw.trimEnd();
|
|
35
|
+
inSpecs = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const indent = raw.length - raw.trimStart().length;
|
|
39
|
+
const line = raw.trim();
|
|
40
|
+
if (section === "DEPENDENCIES" && indent === 2) {
|
|
41
|
+
const name = /^([^\s!(]+)/.exec(line)?.[1];
|
|
42
|
+
if (name)
|
|
43
|
+
direct.push(name);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (line === "specs:" && indent === 2) {
|
|
47
|
+
inSpecs = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (indent === 2) {
|
|
51
|
+
inSpecs = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!inSpecs || indent !== 4 || section !== "GEM")
|
|
55
|
+
continue;
|
|
56
|
+
const open = line.indexOf(" (");
|
|
57
|
+
if (open <= 0 || !line.endsWith(")"))
|
|
58
|
+
continue;
|
|
59
|
+
const name = line.slice(0, open);
|
|
60
|
+
const version = line.slice(open + 2, -1).split("-")[0].trim();
|
|
61
|
+
if (!name || name.includes(" ") || !version)
|
|
62
|
+
continue;
|
|
63
|
+
instances.add(name, version);
|
|
64
|
+
if (!versions.has(name))
|
|
65
|
+
versions.set(name, version);
|
|
66
|
+
}
|
|
67
|
+
return { instances, versions, direct };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* composer.lock: `packages` (runtime) and `packages-dev` (dev). A leading `v`
|
|
71
|
+
* is dropped (`v5.4.0` is release 5.4.0 on Packagist and in OSV). A branch
|
|
72
|
+
* checkout (`dev-main`) names no version and is skipped; a numbered branch
|
|
73
|
+
* alias (`3.0.x-dev`) is kept, as osv-scanner and the desktop app keep it.
|
|
74
|
+
*/
|
|
75
|
+
export function readComposerLock(lock) {
|
|
76
|
+
const instances = new InstanceSet();
|
|
77
|
+
const versions = new Map();
|
|
78
|
+
for (const [key, dev] of [["packages", false], ["packages-dev", true]]) {
|
|
79
|
+
const list = lock[key];
|
|
80
|
+
if (!Array.isArray(list))
|
|
81
|
+
continue;
|
|
82
|
+
for (const entry of list) {
|
|
83
|
+
if (typeof entry?.name !== "string" || typeof entry.version !== "string")
|
|
84
|
+
continue;
|
|
85
|
+
const version = entry.version.replace(/^v(?=\d)/, "");
|
|
86
|
+
if (!entry.name || !/^\d/.test(version))
|
|
87
|
+
continue;
|
|
88
|
+
instances.add(entry.name, version, dev);
|
|
89
|
+
if (!versions.has(entry.name))
|
|
90
|
+
versions.set(entry.name, version);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { instances, versions };
|
|
94
|
+
}
|
|
95
|
+
export function resolveRuby(cwd) {
|
|
96
|
+
const lockPath = path.join(cwd, "Gemfile.lock");
|
|
97
|
+
if (!fs.existsSync(lockPath))
|
|
98
|
+
return emptySource();
|
|
99
|
+
const read = readGemfileLock(fs.readFileSync(lockPath, "utf-8"));
|
|
100
|
+
if (read.instances.size === 0)
|
|
101
|
+
return emptySource();
|
|
102
|
+
return found(read.versions, read.instances, lockPath, "lockfile", { direct: { deps: read.direct, devDeps: [] } });
|
|
103
|
+
}
|
|
104
|
+
export function resolvePhp(cwd) {
|
|
105
|
+
const lockPath = path.join(cwd, "composer.lock");
|
|
106
|
+
if (!fs.existsSync(lockPath))
|
|
107
|
+
return emptySource();
|
|
108
|
+
const read = readComposerLock(JSON.parse(fs.readFileSync(lockPath, "utf-8")));
|
|
109
|
+
if (read.instances.size === 0)
|
|
110
|
+
return emptySource();
|
|
111
|
+
return found(read.versions, read.instances, lockPath, "lockfile", { direct: composerDirect(cwd) });
|
|
112
|
+
}
|
|
113
|
+
/** composer.json `require` / `require-dev` package names (platform requirements such as `php` and `ext-*` left out). */
|
|
114
|
+
function composerDirect(cwd) {
|
|
115
|
+
try {
|
|
116
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(cwd, "composer.json"), "utf-8"));
|
|
117
|
+
const names = (field) => Object.keys(manifest[field] ?? {}).filter((n) => n.includes("/"));
|
|
118
|
+
return { deps: names("require"), devDeps: names("require-dev") };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return { deps: [], devDeps: [] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -32,6 +32,12 @@ export type { PackageInstance, VersionSource } from "./lockfile-types.js";
|
|
|
32
32
|
export declare function lockfileCandidates(cwd: string, ecosystem: OsvEcosystem): string[];
|
|
33
33
|
/** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
|
|
34
34
|
export declare function manifestCandidate(cwd: string, ecosystem: OsvEcosystem): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The versions one directory pins for one ecosystem, plus every lockfile that
|
|
37
|
+
* is present but could not be read (`problems`). A damaged or conflicted
|
|
38
|
+
* lockfile used to fall through silently to the next candidate or to "no
|
|
39
|
+
* lockfile" (audit B-9): now the answer names the file and the error.
|
|
40
|
+
*/
|
|
35
41
|
export declare function resolveVersionSource(cwd: string, ecosystem: OsvEcosystem): VersionSource;
|
|
36
42
|
/** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
|
|
37
43
|
export declare function packageNameFromNodeModulesPath(lockPath: string): string;
|
|
@@ -26,14 +26,17 @@
|
|
|
26
26
|
import * as fs from "node:fs";
|
|
27
27
|
import * as path from "node:path";
|
|
28
28
|
import { readBunLock } from "./bun-lockfile.js";
|
|
29
|
-
import { readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
|
|
29
|
+
import { MAX_LOCKFILE_LINE, readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
|
|
30
30
|
import { requirementFiles, resolveGo, resolvePython } from "./lockfile-parsers-pygo.js";
|
|
31
31
|
import { emptySource, found, InstanceSet } from "./lockfile-types.js";
|
|
32
|
+
import { resolvePhp, resolveRuby } from "./lockfile-parsers-rbphp.js";
|
|
32
33
|
const LOCKFILES = {
|
|
33
34
|
npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
|
|
34
35
|
"crates.io": ["Cargo.lock"],
|
|
35
36
|
PyPI: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
|
|
36
37
|
Go: ["go.mod", "go.sum"],
|
|
38
|
+
RubyGems: ["Gemfile.lock"],
|
|
39
|
+
Packagist: ["composer.lock"],
|
|
37
40
|
};
|
|
38
41
|
const MANIFESTS = {
|
|
39
42
|
npm: "package.json",
|
|
@@ -52,7 +55,29 @@ export function manifestCandidate(cwd, ecosystem) {
|
|
|
52
55
|
const name = MANIFESTS[ecosystem];
|
|
53
56
|
return name ? path.join(cwd, name) : null;
|
|
54
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The versions one directory pins for one ecosystem, plus every lockfile that
|
|
60
|
+
* is present but could not be read (`problems`). A damaged or conflicted
|
|
61
|
+
* lockfile used to fall through silently to the next candidate or to "no
|
|
62
|
+
* lockfile" (audit B-9): now the answer names the file and the error.
|
|
63
|
+
*/
|
|
55
64
|
export function resolveVersionSource(cwd, ecosystem) {
|
|
65
|
+
let read;
|
|
66
|
+
let thrown = null;
|
|
67
|
+
try {
|
|
68
|
+
read = readVersionSource(cwd, ecosystem);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
read = emptySource();
|
|
72
|
+
const file = (LOCKFILES[ecosystem] ?? []).map((n) => path.join(cwd, n)).find((f) => fs.existsSync(f));
|
|
73
|
+
if (file)
|
|
74
|
+
thrown = { file, error: describeParseError(file, error) ?? "could not be parsed" };
|
|
75
|
+
}
|
|
76
|
+
// The reader's own problems (a refused requirements include, M4) and the lockfiles it could not read.
|
|
77
|
+
const problems = [...(read.problems ?? []), ...(thrown ? [thrown] : unreadLockfiles(cwd, ecosystem, read))];
|
|
78
|
+
return problems.length > 0 ? { ...read, problems: problems.map((p) => ({ ...p, ecosystem })) } : read;
|
|
79
|
+
}
|
|
80
|
+
function readVersionSource(cwd, ecosystem) {
|
|
56
81
|
switch (ecosystem) {
|
|
57
82
|
case "npm":
|
|
58
83
|
return resolveNpm(cwd);
|
|
@@ -62,10 +87,95 @@ export function resolveVersionSource(cwd, ecosystem) {
|
|
|
62
87
|
return resolvePython(cwd);
|
|
63
88
|
case "Go":
|
|
64
89
|
return resolveGo(cwd);
|
|
90
|
+
case "RubyGems":
|
|
91
|
+
return resolveRuby(cwd);
|
|
92
|
+
case "Packagist":
|
|
93
|
+
return resolvePhp(cwd);
|
|
65
94
|
default:
|
|
66
95
|
return emptySource();
|
|
67
96
|
}
|
|
68
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* yarn.lock entries skipped because a line of theirs is past the reader's
|
|
100
|
+
* length cap (B-10 keeps the patterns linear). Said, so the read is never
|
|
101
|
+
* taken for complete (audit L5).
|
|
102
|
+
*/
|
|
103
|
+
function yarnSkippedProblem(file, skipped) {
|
|
104
|
+
const entries = skipped === 1 ? "1 entry" : `${skipped} entries`;
|
|
105
|
+
return {
|
|
106
|
+
file,
|
|
107
|
+
skipped,
|
|
108
|
+
error: `${entries} on a line longer than ${MAX_LOCKFILE_LINE} characters (not lockfile syntax) ${skipped === 1 ? "was" : "were"} skipped, so ${skipped === 1 ? "its version was" : "their versions were"} not checked`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function yarnSkips(file) {
|
|
112
|
+
try {
|
|
113
|
+
return readYarnLock(fs.readFileSync(file, "utf-8"), new Map());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Lines git writes into a file it could not merge. */
|
|
120
|
+
const CONFLICT_MARKER = /^(?:<{7}|>{7})(?: |$)|^={7}$/m;
|
|
121
|
+
/**
|
|
122
|
+
* Lockfiles that exist here, rank above the file the versions came from (or
|
|
123
|
+
* there is none), and were not read: each with the reason, re-derived from
|
|
124
|
+
* the file so the note can say what is wrong with it.
|
|
125
|
+
*/
|
|
126
|
+
function unreadLockfiles(cwd, ecosystem, read) {
|
|
127
|
+
const readFiles = new Set([read.source, ...(read.extraSources ?? [])].filter((f) => Boolean(f)).map((f) => path.resolve(f)));
|
|
128
|
+
const problems = [];
|
|
129
|
+
for (const name of LOCKFILES[ecosystem] ?? []) {
|
|
130
|
+
if (name === "go.sum")
|
|
131
|
+
continue; // read with go.mod, never on its own
|
|
132
|
+
const file = path.join(cwd, name);
|
|
133
|
+
if (readFiles.has(path.resolve(file)))
|
|
134
|
+
break; // everything after it ranks lower
|
|
135
|
+
if (!fs.existsSync(file))
|
|
136
|
+
continue;
|
|
137
|
+
// A yarn.lock whose entries were all skipped for length: named with the count, even when the file is short (L5).
|
|
138
|
+
const skipped = name === "yarn.lock" ? yarnSkips(file) : 0;
|
|
139
|
+
if (skipped > 0) {
|
|
140
|
+
problems.push(yarnSkippedProblem(file, skipped));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const error = describeParseError(file, null);
|
|
144
|
+
if (error)
|
|
145
|
+
problems.push({ file, error });
|
|
146
|
+
}
|
|
147
|
+
return problems;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Why a present lockfile yielded nothing, or null when nothing is wrong with
|
|
151
|
+
* it (a valid lockfile of a project with no dependencies).
|
|
152
|
+
*/
|
|
153
|
+
function describeParseError(file, error) {
|
|
154
|
+
let content;
|
|
155
|
+
try {
|
|
156
|
+
content = fs.readFileSync(file, "utf-8");
|
|
157
|
+
}
|
|
158
|
+
catch (readError) {
|
|
159
|
+
return `could not be read (${readError instanceof Error ? readError.message : String(readError)})`;
|
|
160
|
+
}
|
|
161
|
+
if (CONFLICT_MARKER.test(content))
|
|
162
|
+
return "it contains git merge-conflict markers (<<<<<<< / >>>>>>>): resolve the conflict and reinstall to regenerate it";
|
|
163
|
+
if (/\.json$/i.test(file)) {
|
|
164
|
+
try {
|
|
165
|
+
JSON.parse(content);
|
|
166
|
+
}
|
|
167
|
+
catch (parseError) {
|
|
168
|
+
return `invalid JSON (${parseError instanceof Error ? parseError.message.slice(0, 120) : String(parseError)})`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (error)
|
|
172
|
+
return `could not be parsed (${error instanceof Error ? error.message.slice(0, 120) : String(error)})`;
|
|
173
|
+
if (/\.json$/i.test(file))
|
|
174
|
+
return null;
|
|
175
|
+
// A text lockfile of some size that gave no package at all is damaged or in a format this reader does not know.
|
|
176
|
+
const lines = content.split(/\r?\n/).filter((l) => l.trim() !== "").length;
|
|
177
|
+
return lines > 20 ? "no package versions could be read from it (unsupported format or damaged)" : null;
|
|
178
|
+
}
|
|
69
179
|
/** Instances for a manifest-only read: one per name, scope unknown. */
|
|
70
180
|
function manifestInstances(versions) {
|
|
71
181
|
return [...versions].map(([name, version]) => ({ name, version }));
|
|
@@ -128,15 +238,17 @@ function resolveNpm(cwd) {
|
|
|
128
238
|
const versions = new Map();
|
|
129
239
|
const instances = new InstanceSet();
|
|
130
240
|
const specs = new Map();
|
|
131
|
-
readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
|
|
241
|
+
const skipped = readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
|
|
132
242
|
// The copy a direct dependency resolves to is the one its package.json range names.
|
|
133
243
|
for (const [name, range] of manifestRanges(cwd)) {
|
|
134
244
|
const version = specs.get(`${name}@${range}`);
|
|
135
245
|
if (version)
|
|
136
246
|
versions.set(name, version);
|
|
137
247
|
}
|
|
138
|
-
if (versions.size > 0)
|
|
139
|
-
|
|
248
|
+
if (versions.size > 0) {
|
|
249
|
+
const read = found(versions, instances, yarnLockPath, "lockfile");
|
|
250
|
+
return skipped > 0 ? { ...read, problems: [yarnSkippedProblem(yarnLockPath, skipped)] } : read;
|
|
251
|
+
}
|
|
140
252
|
}
|
|
141
253
|
catch { /* fall through */ }
|
|
142
254
|
}
|
|
@@ -26,6 +26,33 @@ export interface VersionSource {
|
|
|
26
26
|
extraSources?: string[];
|
|
27
27
|
/** declared_ranges: the declared requirement per name ("^1.2.0"), for names whose version is only a floor. */
|
|
28
28
|
ranges?: Map<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* The direct dependencies the lockfile itself names (Gemfile.lock
|
|
31
|
+
* DEPENDENCIES, composer.json require / require-dev), for ecosystems whose
|
|
32
|
+
* manifest the project scanner does not read.
|
|
33
|
+
*/
|
|
34
|
+
direct?: {
|
|
35
|
+
deps: string[];
|
|
36
|
+
devDeps: string[];
|
|
37
|
+
};
|
|
38
|
+
/** Lockfiles present here that could not be read, and why: never a silent "no lockfile". */
|
|
39
|
+
problems?: LockfileProblem[];
|
|
40
|
+
}
|
|
41
|
+
/** A lockfile that exists but could not be read. */
|
|
42
|
+
export interface LockfileProblem {
|
|
43
|
+
/** Absolute path. */
|
|
44
|
+
file: string;
|
|
45
|
+
/** Why, in words ("merge-conflict markers", "invalid JSON: ..."). */
|
|
46
|
+
error: string;
|
|
47
|
+
/**
|
|
48
|
+
* Set when the file WAS read except for this many entries (a yarn.lock line
|
|
49
|
+
* past the length cap): a partial read, not an unreadable file.
|
|
50
|
+
*/
|
|
51
|
+
skipped?: number;
|
|
52
|
+
/** Set for a requirements include refused because it points outside the project (B-11): a policy, not damage. */
|
|
53
|
+
refused?: true;
|
|
54
|
+
/** The ecosystem whose resolution reported it (stamped by resolveVersionSource), for ecosystem_filter. */
|
|
55
|
+
ecosystem?: string;
|
|
29
56
|
}
|
|
30
57
|
export type SourceKind = "lockfile" | "manifest" | "declared_ranges";
|
|
31
58
|
export declare function emptySource(): VersionSource;
|
|
@@ -40,4 +67,4 @@ export declare class InstanceSet {
|
|
|
40
67
|
get size(): number;
|
|
41
68
|
toArray(): PackageInstance[];
|
|
42
69
|
}
|
|
43
|
-
export declare function found(versions: Map<string, string>, instances: InstanceSet | PackageInstance[], source: string, kind: SourceKind, extra?: Pick<VersionSource, "extraSources" | "ranges">): VersionSource;
|
|
70
|
+
export declare function found(versions: Map<string, string>, instances: InstanceSet | PackageInstance[], source: string, kind: SourceKind, extra?: Pick<VersionSource, "extraSources" | "ranges" | "direct">): VersionSource;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
|
|
4
4
|
* dependency_health) so they all draw the same line.
|
|
5
5
|
*/
|
|
6
|
-
import type { VulnerabilityEntry } from "./types.js";
|
|
6
|
+
import type { OsvVulnerability, VulnerabilityEntry } from "./types.js";
|
|
7
7
|
/**
|
|
8
8
|
* Is this advisory a maintenance notice rather than a vulnerability?
|
|
9
9
|
*
|
|
@@ -20,9 +20,23 @@ import type { VulnerabilityEntry } from "./types.js";
|
|
|
20
20
|
*
|
|
21
21
|
* Matched on the advisory's own wording, which RustSec keeps consistent, and
|
|
22
22
|
* deliberately NOT on "deprecated" — that word appears in plenty of real
|
|
23
|
-
* vulnerability summaries.
|
|
23
|
+
* vulnerability summaries. An entry the scan marked from the record's
|
|
24
|
+
* `informational` field (`maintenance`, isMaintenanceRecord) is one whatever
|
|
25
|
+
* its wording.
|
|
24
26
|
*/
|
|
25
|
-
export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary">
|
|
27
|
+
export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary"> & {
|
|
28
|
+
maintenance?: boolean;
|
|
29
|
+
}): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The same predicate over an OSV record, for one package (the rule both engines
|
|
32
|
+
* share, vectors ft-074 / sv-024): the summary wording, OR `informational` of
|
|
33
|
+
* `unmaintained` / `notice` on the record, OR on an `affected[]` entry for this
|
|
34
|
+
* package. RustSec writes it per affected entry, never on the record:
|
|
35
|
+
* RUSTSEC-2018-0017 (`tempdir` crate has been deprecated; use `tempfile`)
|
|
36
|
+
* carries it only there, and was graded a vulnerability while only the record
|
|
37
|
+
* level was read.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isMaintenanceRecord(vuln: Pick<OsvVulnerability, "summary" | "database_specific" | "affected">, packageName: string, ecosystem: string): boolean;
|
|
26
40
|
/**
|
|
27
41
|
* The advisories a scan can actually ask the user to act on today: built on
|
|
28
42
|
* this host AND exploitable (not a maintenance notice). This is the set
|
|
@@ -30,4 +44,6 @@ export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary
|
|
|
30
44
|
* every other tool that counts "vulnerable" must count the same set or the
|
|
31
45
|
* numbers disagree across tools over one scan.
|
|
32
46
|
*/
|
|
33
|
-
export declare function isActionableVulnerability(v: Pick<VulnerabilityEntry, "summary" | "platformActive">
|
|
47
|
+
export declare function isActionableVulnerability(v: Pick<VulnerabilityEntry, "summary" | "platformActive"> & {
|
|
48
|
+
maintenance?: boolean;
|
|
49
|
+
}): boolean;
|
package/dist/live/maintenance.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
|
|
5
5
|
* dependency_health) so they all draw the same line.
|
|
6
6
|
*/
|
|
7
|
+
import { samePackageName } from "./version-compare.js";
|
|
8
|
+
/** The `informational` kinds that make a notice. `unsound` is a vulnerability: it has real fixes. */
|
|
9
|
+
const NOTICE_KINDS = new Set(["unmaintained", "notice"]);
|
|
10
|
+
const NOTICE_WORDING = /\b(unmaintained|no longer maintained)\b/i;
|
|
7
11
|
/**
|
|
8
12
|
* Is this advisory a maintenance notice rather than a vulnerability?
|
|
9
13
|
*
|
|
@@ -20,10 +24,35 @@
|
|
|
20
24
|
*
|
|
21
25
|
* Matched on the advisory's own wording, which RustSec keeps consistent, and
|
|
22
26
|
* deliberately NOT on "deprecated" — that word appears in plenty of real
|
|
23
|
-
* vulnerability summaries.
|
|
27
|
+
* vulnerability summaries. An entry the scan marked from the record's
|
|
28
|
+
* `informational` field (`maintenance`, isMaintenanceRecord) is one whatever
|
|
29
|
+
* its wording.
|
|
24
30
|
*/
|
|
25
31
|
export function isMaintenanceNotice(v) {
|
|
26
|
-
|
|
32
|
+
// The scan decided from the record (isMaintenanceRecord); only an entry without that answer
|
|
33
|
+
// falls back to the wording.
|
|
34
|
+
return typeof v.maintenance === "boolean" ? v.maintenance : NOTICE_WORDING.test(v.summary || "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The same predicate over an OSV record, for one package (the rule both engines
|
|
38
|
+
* share, vectors ft-074 / sv-024): the summary wording, OR `informational` of
|
|
39
|
+
* `unmaintained` / `notice` on the record, OR on an `affected[]` entry for this
|
|
40
|
+
* package. RustSec writes it per affected entry, never on the record:
|
|
41
|
+
* RUSTSEC-2018-0017 (`tempdir` crate has been deprecated; use `tempfile`)
|
|
42
|
+
* carries it only there, and was graded a vulnerability while only the record
|
|
43
|
+
* level was read.
|
|
44
|
+
*/
|
|
45
|
+
export function isMaintenanceRecord(vuln, packageName, ecosystem) {
|
|
46
|
+
// Structured data beats wording (the rule as amended 2026-10-26, 4DA fix/notice-class-beats-wording):
|
|
47
|
+
// a class the record states for this package decides alone, the affected[] entry's first, else
|
|
48
|
+
// the record's. `unsound` is never a notice, whatever its summary says: serde_yml's
|
|
49
|
+
// unsound advisory reads "unmaintained" and was hidden as a notice that never blocks
|
|
50
|
+
// (audit 6.2.5 F-7). The wording counts only when no class is stated.
|
|
51
|
+
const own = (vuln.affected ?? []).find((a) => a.package?.ecosystem === ecosystem && samePackageName(a.package.name, packageName, ecosystem) && a.database_specific?.informational)?.database_specific?.informational;
|
|
52
|
+
const stated = own ?? vuln.database_specific?.informational;
|
|
53
|
+
if (stated)
|
|
54
|
+
return NOTICE_KINDS.has(stated);
|
|
55
|
+
return NOTICE_WORDING.test(vuln.summary ?? "");
|
|
27
56
|
}
|
|
28
57
|
/**
|
|
29
58
|
* The advisories a scan can actually ask the user to act on today: built on
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an advisory is a malicious-package record: THE predicate, decided by the 4DA lead for
|
|
3
|
+
* both engines (2026-10-18). A MAL- id (OpenSSF's malicious-packages feed), a MAL- alias, or
|
|
4
|
+
* CWE-506 ("Embedded Malicious Code") in the record's `database_specific.cwe_ids`. GitHub's
|
|
5
|
+
* records of npm's removed malware carry no MAL- id: GHSA-c2m4-w5hm-vqjw ("crossenv is malware")
|
|
6
|
+
* has aliases [CVE-2017-16074] and cwe_ids ["CWE-506"]. GitHub's older malware records carry
|
|
7
|
+
* neither: GHSA-28f8-hqmc-7ph8 "Malicious Package in ember-power-timepicker" (reviewed CRITICAL)
|
|
8
|
+
* has aliases [] and cwe_ids [], one of 49 such npm records, so GitHub's fixed advisory title
|
|
9
|
+
* template counts too: a summary that STARTS "Malicious Package in " or "Malicious code in "
|
|
10
|
+
* (any case). Free prose that merely mentions "malicious" or "malware" does not.
|
|
11
|
+
*
|
|
12
|
+
* Every malware decision goes through it: the offered-release check, the clean-release predicate
|
|
13
|
+
* (a release it affects is never clean, never a target, never "newest"), parent standing, the
|
|
14
|
+
* security-holding note and the severity rank (an installed malicious copy ranks critical).
|
|
15
|
+
*/
|
|
16
|
+
export declare function isMalware(id: string, aliases?: readonly string[], cweIds?: readonly unknown[], summary?: string): boolean;
|
|
17
|
+
/** The CWE ids an OSV record names (`database_specific.cwe_ids`), as strings. */
|
|
18
|
+
export declare function cweIdsOf(record: {
|
|
19
|
+
database_specific?: unknown;
|
|
20
|
+
}): string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Whether an advisory is a malicious-package record: THE predicate, decided by the 4DA lead for
|
|
4
|
+
* both engines (2026-10-18). A MAL- id (OpenSSF's malicious-packages feed), a MAL- alias, or
|
|
5
|
+
* CWE-506 ("Embedded Malicious Code") in the record's `database_specific.cwe_ids`. GitHub's
|
|
6
|
+
* records of npm's removed malware carry no MAL- id: GHSA-c2m4-w5hm-vqjw ("crossenv is malware")
|
|
7
|
+
* has aliases [CVE-2017-16074] and cwe_ids ["CWE-506"]. GitHub's older malware records carry
|
|
8
|
+
* neither: GHSA-28f8-hqmc-7ph8 "Malicious Package in ember-power-timepicker" (reviewed CRITICAL)
|
|
9
|
+
* has aliases [] and cwe_ids [], one of 49 such npm records, so GitHub's fixed advisory title
|
|
10
|
+
* template counts too: a summary that STARTS "Malicious Package in " or "Malicious code in "
|
|
11
|
+
* (any case). Free prose that merely mentions "malicious" or "malware" does not.
|
|
12
|
+
*
|
|
13
|
+
* Every malware decision goes through it: the offered-release check, the clean-release predicate
|
|
14
|
+
* (a release it affects is never clean, never a target, never "newest"), parent standing, the
|
|
15
|
+
* security-holding note and the severity rank (an installed malicious copy ranks critical).
|
|
16
|
+
*/
|
|
17
|
+
export function isMalware(id, aliases = [], cweIds = [], summary = "") {
|
|
18
|
+
// Exactly fourda_depengine::is_malware (4DA #992, b18f364f5): ids and CWEs trimmed and
|
|
19
|
+
// case-insensitive; the summary trimStart-ed, lowercased, then matched at the start.
|
|
20
|
+
if ([id, ...aliases].some((x) => typeof x === "string" && x.trim().toUpperCase().startsWith("MAL-")))
|
|
21
|
+
return true;
|
|
22
|
+
if (cweIds.some((c) => typeof c === "string" && c.trim().toUpperCase() === "CWE-506"))
|
|
23
|
+
return true;
|
|
24
|
+
const title = (summary ?? "").trimStart().toLowerCase();
|
|
25
|
+
return title.startsWith("malicious package in ") || title.startsWith("malicious code in ");
|
|
26
|
+
}
|
|
27
|
+
/** The CWE ids an OSV record names (`database_specific.cwe_ids`), as strings. */
|
|
28
|
+
export function cweIdsOf(record) {
|
|
29
|
+
const ids = record.database_specific?.cwe_ids;
|
|
30
|
+
return Array.isArray(ids) ? ids.filter((x) => typeof x === "string") : [];
|
|
31
|
+
}
|
|
@@ -29,11 +29,14 @@ interface NpmVersionDoc {
|
|
|
29
29
|
hasInstallScript?: boolean;
|
|
30
30
|
dependencies?: Record<string, string>;
|
|
31
31
|
optionalDependencies?: Record<string, string>;
|
|
32
|
-
deprecated?: string;
|
|
32
|
+
deprecated?: string | boolean;
|
|
33
33
|
}
|
|
34
34
|
export interface NpmPackument {
|
|
35
35
|
versions?: Record<string, NpmVersionDoc>;
|
|
36
36
|
time?: Record<string, unknown>;
|
|
37
|
+
"dist-tags"?: {
|
|
38
|
+
latest?: string;
|
|
39
|
+
};
|
|
37
40
|
}
|
|
38
41
|
export declare class NpmPackumentReader implements ReleaseMetadataSource {
|
|
39
42
|
private cache;
|
|
@@ -29,7 +29,8 @@ export class NpmPackumentReader {
|
|
|
29
29
|
this.rateLimiter = rateLimiter;
|
|
30
30
|
}
|
|
31
31
|
async getReleases(name) {
|
|
32
|
-
|
|
32
|
+
// v2: the projection carries `latest` (FX-H5); a v1 row revalidated by a 304 would lack it.
|
|
33
|
+
const key = `npm-packument:v2:${name}`;
|
|
33
34
|
const cached = this.cache.getStale(key)?.data ?? null;
|
|
34
35
|
if (cached && Date.now() - cached.fetchedAtMs < FRESH_MS) {
|
|
35
36
|
return { status: "ok", data: cached.index };
|
|
@@ -85,7 +86,8 @@ export function projectPackument(name, doc) {
|
|
|
85
86
|
trust,
|
|
86
87
|
trustEvidence,
|
|
87
88
|
installScripts: installScriptsOf(v),
|
|
88
|
-
deprecated:
|
|
89
|
+
// `deprecated: true` (no message) counts too, as in the 4DA app.
|
|
90
|
+
deprecated: typeof v.deprecated === "string" && v.deprecated.length > 0 ? v.deprecated : v.deprecated === true ? "deprecated" : null,
|
|
89
91
|
withdrawn: null,
|
|
90
92
|
dependencies: [
|
|
91
93
|
...new Set([...Object.keys(v.dependencies ?? {}), ...Object.keys(v.optionalDependencies ?? {})]),
|
|
@@ -123,6 +125,7 @@ export function projectPackument(name, doc) {
|
|
|
123
125
|
name,
|
|
124
126
|
createdAt: typeof time.created === "string" ? time.created : null,
|
|
125
127
|
releases,
|
|
128
|
+
latest: typeof doc["dist-tags"]?.latest === "string" ? doc["dist-tags"].latest : null,
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
function npmTrust(v) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A release a tool OFFERS as an upgrade (upgrade_planner's target for a row with no advisory,
|
|
3
|
+
* dependency_health's latestStableVersion, what_should_i_know's "latest") is checked the way
|
|
4
|
+
* dependency_check decides `proceed`: against every advisory of the package with the same
|
|
5
|
+
* predicate (osv-ranges.ts advisoriesAffecting) and against the registry's withdrawals. A
|
|
6
|
+
* known-affected or withdrawn release is never offered as a safe upgrade, and the answer says why.
|
|
7
|
+
*
|
|
8
|
+
* Final release audit of 6.2.5 (2026-10-18): upgrade_planner sent crossenv 0.0.2-security, npm's
|
|
9
|
+
* placeholder for a removed malware package, to 1.0.1, which GHSA-c2m4-w5hm-vqjw (last_affected
|
|
10
|
+
* 1.0.1) lists as the malware itself. The same held for cross-env.js, mongose, babelcli, d3.js,
|
|
11
|
+
* jquery.js, ffmepg, nodesass and nodefabric: rows with no advisory on the installed version took
|
|
12
|
+
* the registry's latest stable release with no check at all.
|
|
13
|
+
*/
|
|
14
|
+
import type { FixPathSources } from "./fix-paths.js";
|
|
15
|
+
import type { OsvEcosystem } from "./types.js";
|
|
16
|
+
export interface OfferedRelease {
|
|
17
|
+
/** The release the tool was about to offer. */
|
|
18
|
+
offered: string;
|
|
19
|
+
/** False when the package's advisories could not be read: the offer is unchecked, and said to be. */
|
|
20
|
+
checked: boolean;
|
|
21
|
+
/** Advisories that affect `offered`. */
|
|
22
|
+
affectedBy: string[];
|
|
23
|
+
/** The registry's words when it withdrew `offered` (yanked, or deprecated with a later clean release on its line). */
|
|
24
|
+
withdrawn: string | null;
|
|
25
|
+
/** The release to offer instead: the newest release above the installed one that is neither affected nor withdrawn; null when none. */
|
|
26
|
+
safe: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* The installed version is npm's security placeholder (`0.0.x-security`, published after the
|
|
29
|
+
* malware was taken down) and no release above it is safe: the placeholder is the safe state,
|
|
30
|
+
* and the step is removing the dependency.
|
|
31
|
+
*/
|
|
32
|
+
holding: boolean;
|
|
33
|
+
/** Plain words for an offer that changed or could not be checked; null when `offered` stands as it is. */
|
|
34
|
+
note: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check one offered release. `installed` is the version the project holds; the replacement is
|
|
38
|
+
* looked for above it (a stable one unless nothing stable is newer).
|
|
39
|
+
*/
|
|
40
|
+
export declare function vetOfferedRelease(sources: FixPathSources, ecosystem: OsvEcosystem, name: string, installed: string | null, offered: string): Promise<OfferedRelease>;
|
|
41
|
+
/**
|
|
42
|
+
* vetOfferedRelease over many rows within a time budget; a row not checked in time is absent from
|
|
43
|
+
* the result, and the caller says so. Keyed by `key`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function vetOfferedReleases(sources: FixPathSources | null, items: Array<{
|
|
46
|
+
key: string;
|
|
47
|
+
ecosystem: OsvEcosystem;
|
|
48
|
+
name: string;
|
|
49
|
+
installed: string | null;
|
|
50
|
+
offered: string;
|
|
51
|
+
}>, budgetMs: number, concurrency?: number): Promise<Map<string, OfferedRelease>>;
|
|
52
|
+
/** Said for an offer that could not be checked in time (the budget ran out, or offline). */
|
|
53
|
+
export declare function uncheckedOfferNote(name: string, offered: string): string;
|