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