@superblocksteam/vite-plugin-file-sync 2.0.149 → 2.0.150-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-service/agent/prompts/api-prompts.js +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +38 -10
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +20 -4
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
- package/dist/ai-service/agent/subagents/testing/prompt-builder.js +1 -1
- package/dist/ai-service/agent/tools/apis/analysis.d.ts +6 -0
- package/dist/ai-service/agent/tools/apis/analysis.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/analysis.js +130 -26
- package/dist/ai-service/agent/tools/apis/analysis.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js +2 -2
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js +131 -31
- package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
- package/dist/ai-service/agent/tools/build-delete-file.d.ts +2 -2
- package/dist/ai-service/agent/tools/build-delete-file.js +3 -3
- package/dist/ai-service/agent/tools/build-delete-file.js.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.d.ts +41 -21
- package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.js +299 -20
- package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.js +2 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.js.map +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts +161 -24
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.js +312 -55
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.js.map +1 -1
- package/dist/ai-service/agent/tools/get-logs.d.ts +2 -2
- package/dist/ai-service/agent/tools/report-security-findings.d.ts +3 -3
- package/dist/ai-service/agent/tools.d.ts +10 -11
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +38 -19
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +9 -8
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/grep.js +1 -1
- package/dist/ai-service/agent/tools2/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/index.js +2 -3
- package/dist/ai-service/agent/tools2/tools/index.js.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts +22 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.js +111 -0
- package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -1
- package/dist/ai-service/app-interface/shell.d.ts +71 -3
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +575 -23
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/app-interface/vulnerability-gate.d.ts +83 -0
- package/dist/ai-service/app-interface/vulnerability-gate.d.ts.map +1 -0
- package/dist/ai-service/app-interface/vulnerability-gate.js +543 -0
- package/dist/ai-service/app-interface/vulnerability-gate.js.map +1 -0
- package/dist/ai-service/dev-database-client.d.ts +14 -4
- package/dist/ai-service/dev-database-client.d.ts.map +1 -1
- package/dist/ai-service/dev-database-client.js +1 -24
- package/dist/ai-service/dev-database-client.js.map +1 -1
- package/dist/ai-service/features.d.ts +5 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +5 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/index.d.ts +2 -0
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +60 -9
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/integrations/store.d.ts +3 -0
- package/dist/ai-service/integrations/store.d.ts.map +1 -1
- package/dist/ai-service/integrations/store.js +2 -0
- package/dist/ai-service/integrations/store.js.map +1 -1
- package/dist/ai-service/llm/client.d.ts +2 -9
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +2 -81
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.js +15 -4
- package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -1
- package/dist/ai-service/llm/context-v2/config.d.ts +8 -0
- package/dist/ai-service/llm/context-v2/config.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/config.js +13 -0
- package/dist/ai-service/llm/context-v2/config.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts +18 -0
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.js +94 -2
- package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context.d.ts +3 -0
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +13 -1
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/workflow-usage.d.ts +35 -0
- package/dist/ai-service/llm/stream/observers/workflow-usage.d.ts.map +1 -0
- package/dist/ai-service/llm/stream/observers/workflow-usage.js +108 -0
- package/dist/ai-service/llm/stream/observers/workflow-usage.js.map +1 -0
- package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.js +1 -0
- package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
- package/dist/ai-service/llm/stream/session.d.ts +2 -0
- package/dist/ai-service/llm/stream/session.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/session.js +2 -0
- package/dist/ai-service/llm/stream/session.js.map +1 -1
- package/dist/ai-service/managed-native-db-availability.d.ts +62 -0
- package/dist/ai-service/managed-native-db-availability.d.ts.map +1 -0
- package/dist/ai-service/managed-native-db-availability.js +37 -0
- package/dist/ai-service/managed-native-db-availability.js.map +1 -0
- package/dist/ai-service/security/vulnerability-triage.d.ts +90 -0
- package/dist/ai-service/security/vulnerability-triage.d.ts.map +1 -0
- package/dist/ai-service/security/vulnerability-triage.js +113 -0
- package/dist/ai-service/security/vulnerability-triage.js.map +1 -0
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js +13 -0
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +12 -0
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +23 -0
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.js +17 -27
- package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/idle.d.ts +1 -1
- package/dist/ai-service/state-machine/handlers/idle.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/idle.js +28 -2
- package/dist/ai-service/state-machine/handlers/idle.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/play-dead.d.ts +2 -2
- package/dist/ai-service/state-machine/handlers/play-dead.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/play-dead.js +18 -4
- package/dist/ai-service/state-machine/handlers/play-dead.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/prompt-billing-metrics.d.ts +26 -0
- package/dist/ai-service/state-machine/helpers/prompt-billing-metrics.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/prompt-billing-metrics.js +127 -0
- package/dist/ai-service/state-machine/helpers/prompt-billing-metrics.js.map +1 -0
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts +36 -3
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js +119 -11
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js.map +1 -1
- package/dist/ai-service/tasks/factory.d.ts +2 -0
- package/dist/ai-service/tasks/factory.d.ts.map +1 -1
- package/dist/ai-service/tasks/factory.js +7 -0
- package/dist/ai-service/tasks/factory.js.map +1 -1
- package/dist/ai-service/types.d.ts +57 -3
- package/dist/ai-service/types.d.ts.map +1 -1
- package/dist/ai-service/types.js +20 -0
- package/dist/ai-service/types.js.map +1 -1
- package/dist/migration/restructure.js +1 -1
- package/dist/migration-templates/app-fullstack/client/pages/{Page1 → Home}/index.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/router.tsx +1 -1
- package/dist/migration-templates/app-fullstack/package.json +2 -2
- package/dist/npm/apply-package-json-substitutions.d.ts +19 -0
- package/dist/npm/apply-package-json-substitutions.d.ts.map +1 -0
- package/dist/npm/apply-package-json-substitutions.js +108 -0
- package/dist/npm/apply-package-json-substitutions.js.map +1 -0
- package/dist/policy-gate-callback-mapper.d.ts +1 -1
- package/dist/policy-gate-callback-mapper.d.ts.map +1 -1
- package/dist/policy-gate-callback-mapper.js +5 -6
- package/dist/policy-gate-callback-mapper.js.map +1 -1
- package/dist/policy-gate-runner.d.ts.map +1 -1
- package/dist/policy-gate-runner.js +22 -4
- package/dist/policy-gate-runner.js.map +1 -1
- package/package.json +8 -8
|
@@ -32,16 +32,19 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
-
import { exec } from "node:child_process";
|
|
35
|
+
import { exec, execFile } from "node:child_process";
|
|
36
36
|
import { readFile as fsReadFile, mkdir, writeFile, access, rm, stat, readdir, } from "node:fs/promises";
|
|
37
37
|
import * as path from "node:path";
|
|
38
38
|
import { promisify } from "node:util";
|
|
39
|
+
import { metrics } from "@opentelemetry/api";
|
|
39
40
|
import { glob } from "glob";
|
|
40
41
|
import JSON5 from "json5";
|
|
41
42
|
import { detect, resolveCommand } from "package-manager-detector";
|
|
43
|
+
import semver from "semver";
|
|
42
44
|
import { normalizePath } from "vite";
|
|
43
45
|
import { isProtectedCorePackage } from "@superblocksteam/library-shared/types";
|
|
44
46
|
import { bucketNpmRegistryHost, recordNpmInstall, } from "@superblocksteam/telemetry";
|
|
47
|
+
import { applyPackageJsonSubstitutions, readDeclaredDependencies, } from "../../npm/apply-package-json-substitutions.js";
|
|
45
48
|
import { preparePackageJsonForCloudNpm } from "../../npm/prepare-package-json-for-cloud-npm.js";
|
|
46
49
|
import { getErrorMeta, getLogger } from "../../util/logger.js";
|
|
47
50
|
import llmobs from "../llmobs/index.js";
|
|
@@ -52,7 +55,9 @@ import { DraftManager, validateFileOperation, } from "./filesystem/index.js";
|
|
|
52
55
|
import { Linter } from "./linter.js";
|
|
53
56
|
import { parseNpmJsonDiagnostic, parseNpmJsonError, parseNpmJsonSuccess, } from "./npm-error-parser.js";
|
|
54
57
|
import { prepareForPrivateRegistry, shouldIgnoreInstallScripts, } from "./npm-registry.js";
|
|
58
|
+
import { applyPackageSubstitutions, runVulnerabilityGate, } from "./vulnerability-gate.js";
|
|
55
59
|
const execPromise = promisify(exec);
|
|
60
|
+
const execFilePromise = promisify(execFile);
|
|
56
61
|
// `npm install --json` emits the full install summary on stdout. The default
|
|
57
62
|
// 1 MiB exec buffer can be exceeded for fat dependency trees, which would
|
|
58
63
|
// fail a successful install with ERR_CHILD_PROCESS_STDIO_MAXBUFFER and reach
|
|
@@ -61,6 +66,79 @@ const execPromise = promisify(exec);
|
|
|
61
66
|
// covers real-world npm summaries (per-package entries are tiny) while
|
|
62
67
|
// keeping the worst-case buffer + parsed-object peak bounded.
|
|
63
68
|
const NPM_JSON_MAX_BUFFER = 4 * 1024 * 1024;
|
|
69
|
+
/** Lowest version a declared range admits; skips `workspace:`/`file:`/`*`/git. */
|
|
70
|
+
function declaredRangeFloor(range) {
|
|
71
|
+
if (range == null || semver.validRange(range) == null)
|
|
72
|
+
return null;
|
|
73
|
+
const floor = semver.minVersion(range);
|
|
74
|
+
if (floor == null)
|
|
75
|
+
return null;
|
|
76
|
+
// `*` floors to 0.0.0; an exact `0.0.0` pin is real.
|
|
77
|
+
if (floor.version === "0.0.0" && semver.valid(range) == null)
|
|
78
|
+
return null;
|
|
79
|
+
return floor.version;
|
|
80
|
+
}
|
|
81
|
+
function emitIndexUnavailableMetric() {
|
|
82
|
+
metrics
|
|
83
|
+
.getMeter("superblocks.clark.pkg_security")
|
|
84
|
+
.createCounter("superblocks.clark.pkg_security.index_unavailable", {
|
|
85
|
+
description: "Vulnerability index unavailable during Clark package-install gate",
|
|
86
|
+
})
|
|
87
|
+
.add(1);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* npm `--json` stdout can be prefixed with non-JSON status lines on some
|
|
91
|
+
* versions (`add foo 1.2.3\n{...}`). Slice from the first `{`.
|
|
92
|
+
*/
|
|
93
|
+
function parseNpmJsonStdout(stdout) {
|
|
94
|
+
if (typeof stdout !== "string" || stdout.length === 0) {
|
|
95
|
+
throw new Error("npm --json stdout contained no JSON object");
|
|
96
|
+
}
|
|
97
|
+
const start = stdout.indexOf("{");
|
|
98
|
+
if (start < 0) {
|
|
99
|
+
throw new Error("npm --json stdout contained no JSON object");
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(stdout.slice(start));
|
|
102
|
+
}
|
|
103
|
+
/** True for npm `--json` failure bodies (`{ "error": ... }`), not dry-run trees. */
|
|
104
|
+
function isNpmErrorEnvelope(parsed) {
|
|
105
|
+
if (parsed == null || typeof parsed !== "object")
|
|
106
|
+
return false;
|
|
107
|
+
if (!("error" in parsed))
|
|
108
|
+
return false;
|
|
109
|
+
return parsed.error != null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* True for an npm dry-run entry installed into the app's own `node_modules`
|
|
113
|
+
* (`<app>/node_modules/lodash`) rather than nested under a dependency
|
|
114
|
+
* (`<app>/node_modules/@scope/library/node_modules/lodash`). npm omits `path`
|
|
115
|
+
* on some shapes; treat those as top-level so a missing field cannot hide a
|
|
116
|
+
* package from the top-level CVE checks.
|
|
117
|
+
*/
|
|
118
|
+
function isRootNodeModulesPath(entryPath) {
|
|
119
|
+
if (typeof entryPath !== "string" || entryPath.length === 0)
|
|
120
|
+
return true;
|
|
121
|
+
return entryPath.replace(/\\/g, "/").split("/node_modules/").length <= 2;
|
|
122
|
+
}
|
|
123
|
+
function packageNameFromLockPath(lockPath) {
|
|
124
|
+
if (!lockPath)
|
|
125
|
+
return undefined;
|
|
126
|
+
const marker = "node_modules/";
|
|
127
|
+
const idx = lockPath.lastIndexOf(marker);
|
|
128
|
+
if (idx < 0)
|
|
129
|
+
return undefined;
|
|
130
|
+
const name = lockPath.slice(idx + marker.length);
|
|
131
|
+
return name.length > 0 ? name : undefined;
|
|
132
|
+
}
|
|
133
|
+
function isPackageLookupResult(value) {
|
|
134
|
+
if (value == null || typeof value !== "object")
|
|
135
|
+
return false;
|
|
136
|
+
const record = value;
|
|
137
|
+
return (typeof record.name === "string" &&
|
|
138
|
+
typeof record.version === "string" &&
|
|
139
|
+
typeof record.hasMalware === "boolean" &&
|
|
140
|
+
Array.isArray(record.advisories));
|
|
141
|
+
}
|
|
64
142
|
/**
|
|
65
143
|
* Map an install failure to a value in the closed `npm.outcome` enum
|
|
66
144
|
* (APPS-4189). `NpmInstallBlocked` carries the structured reason from
|
|
@@ -194,12 +272,18 @@ let AppShell = (() => {
|
|
|
194
272
|
* The reporter is fire-and-forget — see `maybeReportBlockedInstall`.
|
|
195
273
|
*/
|
|
196
274
|
reportNpmInstallBlocked;
|
|
275
|
+
/**
|
|
276
|
+
* Optional deps for the install-time vulnerability index lookup.
|
|
277
|
+
* When absent the malware/CVE gate is skipped (test fixtures / deliberate
|
|
278
|
+
* opt-out). Production AiService always wires these.
|
|
279
|
+
*/
|
|
280
|
+
packageLookup;
|
|
197
281
|
/**
|
|
198
282
|
* Tracks files that must be read before they can be written.
|
|
199
283
|
* Used to enforce read-before-write pattern for AI safety.
|
|
200
284
|
*/
|
|
201
285
|
requiredReads = new Set();
|
|
202
|
-
constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, npmRegistryClient, reportNpmInstallBlocked, }) {
|
|
286
|
+
constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, npmRegistryClient, reportNpmInstallBlocked, packageLookup, }) {
|
|
203
287
|
// Normalize path to handle Windows drive letter case
|
|
204
288
|
this.appRootDirPath = normalizePath(appRootDirPath);
|
|
205
289
|
this.fsOperationQueue = fsOperationQueue;
|
|
@@ -214,6 +298,7 @@ let AppShell = (() => {
|
|
|
214
298
|
this.enableDeletingLockfiles = enableDeletingLockfiles ?? false;
|
|
215
299
|
this.npmRegistryClient = npmRegistryClient;
|
|
216
300
|
this.reportNpmInstallBlocked = reportNpmInstallBlocked;
|
|
301
|
+
this.packageLookup = packageLookup;
|
|
217
302
|
}
|
|
218
303
|
/**
|
|
219
304
|
* Toggle the API virtual-file projection. When disabled, reads to `apis/*`
|
|
@@ -658,25 +743,118 @@ let AppShell = (() => {
|
|
|
658
743
|
* server throttles + sanitizes + persists. Fire-and-forget: a reporting
|
|
659
744
|
* failure must never surface to (or break) the install caller.
|
|
660
745
|
*/
|
|
746
|
+
safeAudit(report) {
|
|
747
|
+
if (!this.reportNpmInstallBlocked)
|
|
748
|
+
return;
|
|
749
|
+
try {
|
|
750
|
+
this.reportNpmInstallBlocked(report);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// Audit reporting is best-effort and must not affect the install.
|
|
754
|
+
}
|
|
755
|
+
}
|
|
661
756
|
maybeReportBlockedInstall(err, runner) {
|
|
662
757
|
if (!(err instanceof NpmInstallBlocked))
|
|
663
758
|
return;
|
|
664
|
-
|
|
759
|
+
// Audit only real CVE/malware findings — index/probe failures are fail-closed
|
|
760
|
+
// blocks, not malware hits, and must not pollute SIEM as vulnerabilities.
|
|
761
|
+
const isAuditableFinding = (finding) => finding.kind === "cve" || finding.kind === "malware";
|
|
762
|
+
const vulnerabilities = err.findings?.filter(isAuditableFinding).map((finding) => ({
|
|
763
|
+
advisoryId: finding.advisoryId,
|
|
764
|
+
kind: finding.kind,
|
|
765
|
+
package: finding.package,
|
|
766
|
+
...(finding.severity !== undefined
|
|
767
|
+
? { severity: finding.severity }
|
|
768
|
+
: {}),
|
|
769
|
+
...(finding.suggestedVersion !== undefined
|
|
770
|
+
? { suggestedVersion: finding.suggestedVersion }
|
|
771
|
+
: {}),
|
|
772
|
+
}));
|
|
773
|
+
// A block whose findings are all index/probe outages still carries
|
|
774
|
+
// `reason: vulnerable_package`, so auditing it would file an outage as a
|
|
775
|
+
// CVE hit. The `index_unavailable` metric already covers those.
|
|
776
|
+
if (err.findings?.length && !vulnerabilities?.length)
|
|
665
777
|
return;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
778
|
+
this.safeAudit({
|
|
779
|
+
reason: err.reason,
|
|
780
|
+
registryHost: err.registryHost,
|
|
781
|
+
packages: err.packages,
|
|
782
|
+
httpStatus: err.httpStatus,
|
|
783
|
+
npmErrorCode: err.npmErrorCode,
|
|
784
|
+
runner,
|
|
785
|
+
occurredAt: Date.now(),
|
|
786
|
+
...(vulnerabilities?.length ? { vulnerabilities } : {}),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
reportNpmTriageVerdict(verdict) {
|
|
790
|
+
this.safeAudit({
|
|
791
|
+
occurredAt: Date.now(),
|
|
792
|
+
packages: [verdict.package],
|
|
793
|
+
reason: "vulnerability_triaged",
|
|
794
|
+
runner: "install_specific",
|
|
795
|
+
triage: verdict,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
reportNpmUserOverride(args) {
|
|
799
|
+
this.safeAudit({
|
|
800
|
+
occurredAt: Date.now(),
|
|
801
|
+
packages: args.packages,
|
|
802
|
+
reason: "vulnerability_user_override",
|
|
803
|
+
runner: "install_specific",
|
|
804
|
+
userOverride: {
|
|
805
|
+
packages: args.packages,
|
|
806
|
+
reason: args.reason,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Report silent CVE/quarantine substitutions to the same audit sink as
|
|
812
|
+
* blocked installs. Best-effort; never fails the install.
|
|
813
|
+
*/
|
|
814
|
+
maybeReportSubstitutions(substitutions, runner) {
|
|
815
|
+
for (const entry of substitutions) {
|
|
816
|
+
this.safeAudit({
|
|
817
|
+
reason: "package_substituted",
|
|
818
|
+
packages: [`${entry.name}@${entry.fromVersion}`],
|
|
673
819
|
runner,
|
|
674
820
|
occurredAt: Date.now(),
|
|
821
|
+
substitution: {
|
|
822
|
+
fromVersion: entry.fromVersion,
|
|
823
|
+
toVersion: entry.toVersion,
|
|
824
|
+
reason: entry.reason,
|
|
825
|
+
},
|
|
675
826
|
});
|
|
676
827
|
}
|
|
677
|
-
|
|
678
|
-
|
|
828
|
+
}
|
|
829
|
+
/** Mode 2: write gate substitutions into package.json. Audit is the caller's job after install succeeds (same as Mode 1). */
|
|
830
|
+
async applyDeclaredDependencyUpgrades(remediation) {
|
|
831
|
+
if (remediation.substitutions.length === 0)
|
|
832
|
+
return [];
|
|
833
|
+
const rewritten = await this.enqueueFsOperation("applyPackageJsonSubstitutions", () => applyPackageJsonSubstitutions({
|
|
834
|
+
appRootDirPath: this.appRootDirPath,
|
|
835
|
+
logger: getLogger(),
|
|
836
|
+
substitutions: remediation.substitutions,
|
|
837
|
+
}));
|
|
838
|
+
if (rewritten.length === 0)
|
|
839
|
+
return [];
|
|
840
|
+
const applied = new Set(rewritten);
|
|
841
|
+
return remediation.substitutions.filter((entry) => applied.has(entry.name));
|
|
842
|
+
}
|
|
843
|
+
async remediateDeclaredDependencies(abortSignal) {
|
|
844
|
+
// Kill switch / unwired lookup: do not short-circuit registry or rewrite
|
|
845
|
+
// package.json — startup must behave as pre-gate.
|
|
846
|
+
if (!this.packageLookup) {
|
|
847
|
+
return;
|
|
679
848
|
}
|
|
849
|
+
await this.withAuthFailedRetry(async (registryFetchResult) => {
|
|
850
|
+
this.maybeShortCircuitForUnreachable(registryFetchResult, []);
|
|
851
|
+
// Cloud: strip workspace: before Mode 2 dry-run malware scan.
|
|
852
|
+
await this.maybePreparePackageJsonForCloudNpm();
|
|
853
|
+
await this.applyDeclaredDependencyUpgrades(await this.maybeRemediateVulnerabilities({
|
|
854
|
+
abortSignal,
|
|
855
|
+
mode: "all",
|
|
856
|
+
}));
|
|
857
|
+
});
|
|
680
858
|
}
|
|
681
859
|
/**
|
|
682
860
|
* Env overlay for AppShell-spawned npm/pnpm installs in the live-edit
|
|
@@ -724,11 +902,14 @@ let AppShell = (() => {
|
|
|
724
902
|
// was at some point configured with a private registry. See
|
|
725
903
|
// `maybeShortCircuitForUnreachable` for the full rationale.
|
|
726
904
|
this.maybeShortCircuitForUnreachable(registryFetchResult, []);
|
|
905
|
+
await this.maybePreparePackageJsonForCloudNpm();
|
|
906
|
+
const appliedSubstitutions = await this.applyDeclaredDependencyUpgrades(await this.maybeRemediateVulnerabilities({
|
|
907
|
+
abortSignal,
|
|
908
|
+
mode: "all",
|
|
909
|
+
}));
|
|
727
910
|
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
728
911
|
const isNpm = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
729
|
-
|
|
730
|
-
await this.maybePreparePackageJsonForCloudNpm();
|
|
731
|
-
}
|
|
912
|
+
// prepare already ran above so Mode 2 cloud dry-run sees semver deps.
|
|
732
913
|
// In csb mock server (local dev) we use pnpm to avoid lockfile / store
|
|
733
914
|
// conflicts with the surrounding workspace; everywhere else we force npm
|
|
734
915
|
// so the `--json` error contract — and therefore the registry-blocked
|
|
@@ -741,7 +922,7 @@ let AppShell = (() => {
|
|
|
741
922
|
const command = ignoreScripts
|
|
742
923
|
? `${baseCommand} --ignore-scripts`
|
|
743
924
|
: baseCommand;
|
|
744
|
-
|
|
925
|
+
await new Promise((resolve, reject) => {
|
|
745
926
|
exec(command, {
|
|
746
927
|
cwd: this.appRootDirPath,
|
|
747
928
|
env: this.installLogEnv(),
|
|
@@ -767,6 +948,9 @@ let AppShell = (() => {
|
|
|
767
948
|
return resolve();
|
|
768
949
|
});
|
|
769
950
|
});
|
|
951
|
+
// Audit only after the install succeeds: a selected substitution is
|
|
952
|
+
// not an applied substitution if npm fails (same as Mode 1).
|
|
953
|
+
this.maybeReportSubstitutions(appliedSubstitutions, "install_all");
|
|
770
954
|
});
|
|
771
955
|
}
|
|
772
956
|
catch (err) {
|
|
@@ -814,6 +998,344 @@ let AppShell = (() => {
|
|
|
814
998
|
hasAnyRegistryConfigured: true,
|
|
815
999
|
});
|
|
816
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* POST `/api/v1/security/package-lookup` for a batch of `{name,version}`
|
|
1003
|
+
* pairs. One request — the server cap is 5000. Throws on non-2xx / network
|
|
1004
|
+
* / malformed body so the caller can apply malware fail-closed.
|
|
1005
|
+
*/
|
|
1006
|
+
async lookupPackageVulnerabilities(packages) {
|
|
1007
|
+
const deps = this.packageLookup;
|
|
1008
|
+
if (!deps) {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
if (packages.length === 0) {
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
const jwt = await deps.getJwt();
|
|
1015
|
+
const base = deps.baseUrl.replace(/\/$/, "");
|
|
1016
|
+
const url = `${base}/api/v1/security/package-lookup`;
|
|
1017
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
1018
|
+
const response = await fetchFn(url, {
|
|
1019
|
+
method: "POST",
|
|
1020
|
+
headers: {
|
|
1021
|
+
Authorization: `Bearer ${jwt}`,
|
|
1022
|
+
"Content-Type": "application/json",
|
|
1023
|
+
},
|
|
1024
|
+
body: JSON.stringify({ packages }),
|
|
1025
|
+
});
|
|
1026
|
+
if (!response.ok) {
|
|
1027
|
+
throw new Error(`package-lookup returned HTTP ${response.status}`);
|
|
1028
|
+
}
|
|
1029
|
+
const body = await response.json();
|
|
1030
|
+
if (body == null ||
|
|
1031
|
+
typeof body !== "object" ||
|
|
1032
|
+
!("data" in body) ||
|
|
1033
|
+
body.data == null ||
|
|
1034
|
+
typeof body.data !== "object" ||
|
|
1035
|
+
!("results" in body.data) ||
|
|
1036
|
+
!Array.isArray(body.data.results)) {
|
|
1037
|
+
throw new Error("package-lookup response missing data.results");
|
|
1038
|
+
}
|
|
1039
|
+
const results = [];
|
|
1040
|
+
for (const entry of body.data.results) {
|
|
1041
|
+
if (!isPackageLookupResult(entry)) {
|
|
1042
|
+
throw new Error("package-lookup response contained an off-shape result");
|
|
1043
|
+
}
|
|
1044
|
+
results.push(entry);
|
|
1045
|
+
}
|
|
1046
|
+
const checked = new Set(results.map((result) => `${result.name}@${result.version}`));
|
|
1047
|
+
for (const pkg of packages) {
|
|
1048
|
+
if (!checked.has(`${pkg.name}@${pkg.version}`)) {
|
|
1049
|
+
throw new Error(`package-lookup response missing result for ${pkg.name}@${pkg.version}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return results;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Mode 1: npm dry-run + lockfile.
|
|
1056
|
+
* Mode 2: `topLevel` is always declared package.json floors (CVE remediations).
|
|
1057
|
+
* Cloud malware tree = floors ∪ package-lock ∪ dry-run (after workspace:
|
|
1058
|
+
* rewrite). Local stays floors-only — pnpm + lingering `workspace:*` mean
|
|
1059
|
+
* full-tree coverage is not available there.
|
|
1060
|
+
*/
|
|
1061
|
+
async resolveInstallTree(mode, packages, abortSignal) {
|
|
1062
|
+
if (mode === "all") {
|
|
1063
|
+
const topLevel = [];
|
|
1064
|
+
for (const pkg of packages ?? []) {
|
|
1065
|
+
const version = declaredRangeFloor(pkg.version);
|
|
1066
|
+
if (version != null)
|
|
1067
|
+
topLevel.push({ name: pkg.name, version });
|
|
1068
|
+
}
|
|
1069
|
+
// Local: floors only. Cloud: union lockfile + dry-run for malware.
|
|
1070
|
+
if (process.env.SUPERBLOCKS_SERVER_ENV === "local") {
|
|
1071
|
+
return { resolves: true, topLevel, tree: topLevel };
|
|
1072
|
+
}
|
|
1073
|
+
const fromLock = await this.readPackagesFromPackageLock();
|
|
1074
|
+
let dryTree = [];
|
|
1075
|
+
try {
|
|
1076
|
+
dryTree = (await this.resolveTreeViaNpmDryRun([], abortSignal)).tree;
|
|
1077
|
+
}
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
// Do not continue remediation after cancel.
|
|
1080
|
+
if (abortSignal?.aborted ||
|
|
1081
|
+
(err instanceof Error && err.name === "AbortError")) {
|
|
1082
|
+
throw err;
|
|
1083
|
+
}
|
|
1084
|
+
// Unexpected probe failure (not ERESOLVE / error envelope) — still
|
|
1085
|
+
// malware-scan floors + lockfile instead of aborting the gate.
|
|
1086
|
+
}
|
|
1087
|
+
const byKey = new Map();
|
|
1088
|
+
for (const pkg of [...(fromLock?.tree ?? []), ...dryTree, ...topLevel]) {
|
|
1089
|
+
byKey.set(`${pkg.name}@${pkg.version}`, pkg);
|
|
1090
|
+
}
|
|
1091
|
+
// Keep resolves:true so a failed dry-run cannot skip floor CVE swaps;
|
|
1092
|
+
// malware still sees whatever floors/lock/dry contributed.
|
|
1093
|
+
return { resolves: true, topLevel, tree: [...byKey.values()] };
|
|
1094
|
+
}
|
|
1095
|
+
const specs = (packages ?? []).map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
|
|
1096
|
+
const resolved = await this.resolveTreeViaNpmDryRun(specs, abortSignal);
|
|
1097
|
+
// Prefer dry-run concrete pins for top-level. Never inject the raw
|
|
1098
|
+
// requested range/`latest` string into the lookup tree — the index
|
|
1099
|
+
// keys on concrete versions, and a range would fail coverage checks.
|
|
1100
|
+
const topLevel = [];
|
|
1101
|
+
for (const pkg of packages ?? []) {
|
|
1102
|
+
// An exact pin is what npm puts at the root, so it needs no resolution —
|
|
1103
|
+
// and looking it up in the dry-run tree risks matching some other copy of
|
|
1104
|
+
// the same name. It stays checkable even when npm cannot resolve the full
|
|
1105
|
+
// tree; the lockfile union below preserves malware coverage for the tree
|
|
1106
|
+
// already on disk.
|
|
1107
|
+
if (pkg.version && semver.valid(pkg.version)) {
|
|
1108
|
+
topLevel.push({ name: pkg.name, version: pkg.version });
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const fromDry = resolved.topLevel.find((entry) => entry.name === pkg.name);
|
|
1112
|
+
if (fromDry)
|
|
1113
|
+
topLevel.push(fromDry);
|
|
1114
|
+
}
|
|
1115
|
+
// Lockfile entries come first so already-satisfied installs (empty
|
|
1116
|
+
// dry-run `add`) still malware-scan locked transitives; dry-run pins and
|
|
1117
|
+
// top-level overwrite on collision because they reflect what this
|
|
1118
|
+
// install actually changes.
|
|
1119
|
+
const fromLock = await this.readPackagesFromPackageLock();
|
|
1120
|
+
const byKey = new Map();
|
|
1121
|
+
for (const pkg of [
|
|
1122
|
+
...(fromLock?.tree ?? []),
|
|
1123
|
+
...resolved.tree,
|
|
1124
|
+
...topLevel,
|
|
1125
|
+
]) {
|
|
1126
|
+
byKey.set(`${pkg.name}@${pkg.version}`, pkg);
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
tree: [...byKey.values()],
|
|
1130
|
+
topLevel,
|
|
1131
|
+
resolves: resolved.resolves,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
async resolveTreeViaNpmDryRun(specs, abortSignal) {
|
|
1135
|
+
const args = [
|
|
1136
|
+
"install",
|
|
1137
|
+
"--dry-run",
|
|
1138
|
+
"--json",
|
|
1139
|
+
"--fund=false",
|
|
1140
|
+
"--audit=false",
|
|
1141
|
+
// `--` so a spec that looks like an option (`--registry=…`,
|
|
1142
|
+
// `--userconfig=…`) is parsed as a package name instead of
|
|
1143
|
+
// reconfiguring the probe: the gate scans the tree this resolves, so a
|
|
1144
|
+
// redirected registry would vouch for packages the real install never
|
|
1145
|
+
// fetches. npm rejects the name and we fail closed.
|
|
1146
|
+
"--",
|
|
1147
|
+
...specs,
|
|
1148
|
+
];
|
|
1149
|
+
try {
|
|
1150
|
+
// execFile (no shell) so hostile package specs cannot inject shell
|
|
1151
|
+
// metacharacters into the dry-run probe.
|
|
1152
|
+
const { stdout } = await execFilePromise("npm", args, {
|
|
1153
|
+
cwd: this.appRootDirPath,
|
|
1154
|
+
env: this.installLogEnv(),
|
|
1155
|
+
signal: abortSignal,
|
|
1156
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
1157
|
+
});
|
|
1158
|
+
const parsed = parseNpmJsonStdout(stdout);
|
|
1159
|
+
return this.treeFromNpmDryRunJson(parsed, specs);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
const err = error;
|
|
1163
|
+
const combined = `${err.stdout ?? ""}\n${err.stderr ?? ""}\n${err.message ?? ""}`;
|
|
1164
|
+
if (/ERESOLVE|EPEERINVALID/i.test(combined)) {
|
|
1165
|
+
return { tree: [], topLevel: [], resolves: false };
|
|
1166
|
+
}
|
|
1167
|
+
// Non-zero exit can still carry a usable dry-run body — or an `{ error }`
|
|
1168
|
+
// envelope (EUNSUPPORTEDPROTOCOL / E404 / ETARGET). Treat the latter as
|
|
1169
|
+
// unresolved so the gate fails closed instead of scanning an empty tree.
|
|
1170
|
+
if (err.stdout && err.stdout.includes("{")) {
|
|
1171
|
+
try {
|
|
1172
|
+
const parsed = parseNpmJsonStdout(err.stdout);
|
|
1173
|
+
if (isNpmErrorEnvelope(parsed)) {
|
|
1174
|
+
return { tree: [], topLevel: [], resolves: false };
|
|
1175
|
+
}
|
|
1176
|
+
return this.treeFromNpmDryRunJson(parsed, specs);
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// fall through
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
treeFromNpmDryRunJson(parsed, specs) {
|
|
1186
|
+
if (parsed == null || typeof parsed !== "object") {
|
|
1187
|
+
return { tree: [], topLevel: [], resolves: true };
|
|
1188
|
+
}
|
|
1189
|
+
const record = parsed;
|
|
1190
|
+
const tree = [];
|
|
1191
|
+
const root = [];
|
|
1192
|
+
const seen = new Set();
|
|
1193
|
+
const push = (name, version, entryPath) => {
|
|
1194
|
+
if (typeof name !== "string" || typeof version !== "string")
|
|
1195
|
+
return;
|
|
1196
|
+
if (!name || !version)
|
|
1197
|
+
return;
|
|
1198
|
+
const key = `${name}@${version}`;
|
|
1199
|
+
if (!seen.has(key)) {
|
|
1200
|
+
seen.add(key);
|
|
1201
|
+
tree.push({ name, version });
|
|
1202
|
+
}
|
|
1203
|
+
if (isRootNodeModulesPath(entryPath))
|
|
1204
|
+
root.push({ name, version });
|
|
1205
|
+
};
|
|
1206
|
+
if (Array.isArray(record.add)) {
|
|
1207
|
+
for (const entry of record.add) {
|
|
1208
|
+
if (entry == null || typeof entry !== "object")
|
|
1209
|
+
continue;
|
|
1210
|
+
const pkg = entry;
|
|
1211
|
+
push(pkg.name, pkg.version, pkg.path);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (Array.isArray(record.change)) {
|
|
1215
|
+
for (const entry of record.change) {
|
|
1216
|
+
if (entry == null || typeof entry !== "object")
|
|
1217
|
+
continue;
|
|
1218
|
+
const change = entry;
|
|
1219
|
+
if (change.to == null || typeof change.to !== "object")
|
|
1220
|
+
continue;
|
|
1221
|
+
const to = change.to;
|
|
1222
|
+
push(to.name, to.version, to.path);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const topLevelNames = new Set(specs.map((spec) => {
|
|
1226
|
+
if (spec.startsWith("@")) {
|
|
1227
|
+
const at = spec.indexOf("@", 1);
|
|
1228
|
+
return at > 0 ? spec.slice(0, at) : spec;
|
|
1229
|
+
}
|
|
1230
|
+
return spec.split("@")[0] ?? spec;
|
|
1231
|
+
}));
|
|
1232
|
+
// Top-level is the copy npm puts at the app's own `node_modules/<name>`.
|
|
1233
|
+
// A dependency's nested copy shares the requested name but is a different
|
|
1234
|
+
// version, and vouching for it would let the requested one install
|
|
1235
|
+
// unchecked.
|
|
1236
|
+
const topLevel = root.filter((pkg) => topLevelNames.has(pkg.name));
|
|
1237
|
+
return { tree, topLevel, resolves: true };
|
|
1238
|
+
}
|
|
1239
|
+
async readPackagesFromPackageLock() {
|
|
1240
|
+
const lockPath = path.join(this.appRootDirPath, "package-lock.json");
|
|
1241
|
+
let raw;
|
|
1242
|
+
try {
|
|
1243
|
+
raw = await fsReadFile(lockPath, "utf8");
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
let parsed;
|
|
1249
|
+
try {
|
|
1250
|
+
parsed = JSON.parse(raw);
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
if (parsed == null || typeof parsed !== "object") {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const lock = parsed;
|
|
1259
|
+
const tree = [];
|
|
1260
|
+
const seen = new Set();
|
|
1261
|
+
const push = (name, version) => {
|
|
1262
|
+
const key = `${name}@${version}`;
|
|
1263
|
+
if (seen.has(key))
|
|
1264
|
+
return;
|
|
1265
|
+
seen.add(key);
|
|
1266
|
+
tree.push({ name, version });
|
|
1267
|
+
};
|
|
1268
|
+
if (lock.packages && typeof lock.packages === "object") {
|
|
1269
|
+
for (const [lockPathKey, meta] of Object.entries(lock.packages)) {
|
|
1270
|
+
if (!meta || typeof meta.version !== "string")
|
|
1271
|
+
continue;
|
|
1272
|
+
// Only `node_modules/**` entries are installed packages. `packages[""]`
|
|
1273
|
+
// (and any workspace root) is first-party code carrying a
|
|
1274
|
+
// builder-chosen name, so scanning it would block every install by an
|
|
1275
|
+
// app that happens to be named after a malicious package.
|
|
1276
|
+
const pathName = packageNameFromLockPath(lockPathKey);
|
|
1277
|
+
if (!pathName)
|
|
1278
|
+
continue;
|
|
1279
|
+
// An aliased install (`"node_modules/alias": { name: "real" }`) fetches
|
|
1280
|
+
// `meta.name` from the registry; that is the name to scan.
|
|
1281
|
+
push(typeof meta.name === "string" && meta.name.length > 0
|
|
1282
|
+
? meta.name
|
|
1283
|
+
: pathName, meta.version);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
else if (lock.dependencies && typeof lock.dependencies === "object") {
|
|
1287
|
+
// lockfileVersion 1: nested `dependencies` hold transitive pins.
|
|
1288
|
+
const walkV1 = (deps) => {
|
|
1289
|
+
if (deps == null || typeof deps !== "object")
|
|
1290
|
+
return;
|
|
1291
|
+
for (const [name, meta] of Object.entries(deps)) {
|
|
1292
|
+
if (meta == null || typeof meta !== "object")
|
|
1293
|
+
continue;
|
|
1294
|
+
if (!("version" in meta) || typeof meta.version !== "string") {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
push(name, meta.version);
|
|
1298
|
+
if ("dependencies" in meta &&
|
|
1299
|
+
meta.dependencies != null &&
|
|
1300
|
+
typeof meta.dependencies === "object") {
|
|
1301
|
+
walkV1(meta.dependencies);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
walkV1(lock.dependencies);
|
|
1306
|
+
}
|
|
1307
|
+
return { tree, topLevel: tree, resolves: true };
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Vulnerability gate. Mode 1 remediates/blocks top-level CVEs; Mode 2
|
|
1311
|
+
* silently rewrites declared deps (same major) and leaves the rest.
|
|
1312
|
+
* No-op when `packageLookup` is unwired. Index failures → malware fail-closed.
|
|
1313
|
+
*/
|
|
1314
|
+
async maybeRemediateVulnerabilities(args) {
|
|
1315
|
+
const { abortSignal, cveUserOverride, mode } = args;
|
|
1316
|
+
if (!this.packageLookup) {
|
|
1317
|
+
return { substitutions: [] };
|
|
1318
|
+
}
|
|
1319
|
+
const packages = mode === "all"
|
|
1320
|
+
? await readDeclaredDependencies(this.appRootDirPath)
|
|
1321
|
+
: args.packages;
|
|
1322
|
+
return await runVulnerabilityGate({
|
|
1323
|
+
abortSignal,
|
|
1324
|
+
capabilities: {
|
|
1325
|
+
lookupPackages: async (resolved) => await this.lookupPackageVulnerabilities(resolved),
|
|
1326
|
+
onIndexUnavailable: ({ error, message }) => {
|
|
1327
|
+
emitIndexUnavailableMetric();
|
|
1328
|
+
getLogger().warn(message, getErrorMeta(error));
|
|
1329
|
+
},
|
|
1330
|
+
pickQuarantineHoldback: this.packageLookup.pickQuarantineHoldback,
|
|
1331
|
+
resolveInstallTree: async (resolveArgs) => await this.resolveInstallTree(resolveArgs.mode, resolveArgs.packages, resolveArgs.abortSignal),
|
|
1332
|
+
resolvePackageSpecs: async (resolveArgs) => await this.resolveTreeViaNpmDryRun(resolveArgs.specs, resolveArgs.abortSignal),
|
|
1333
|
+
},
|
|
1334
|
+
cveUserOverride,
|
|
1335
|
+
mode,
|
|
1336
|
+
packages,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
817
1339
|
/**
|
|
818
1340
|
* Derive the `hasAnyRegistryConfigured` tri-state from the
|
|
819
1341
|
* `NpmRegistryFetchResult` that the immediately-preceding
|
|
@@ -966,14 +1488,14 @@ let AppShell = (() => {
|
|
|
966
1488
|
// APPS-4370: short-circuit before any exec when server is unreachable
|
|
967
1489
|
// AND this org's marker says it was previously configured.
|
|
968
1490
|
this.maybeShortCircuitForUnreachable(registryFetchResult, []);
|
|
969
|
-
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
970
1491
|
// In cloud edit mode, always use npm because pnpm is not available in the container.
|
|
971
1492
|
// The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
|
|
972
1493
|
// from local editing, but we must use npm in cloud mode.
|
|
973
1494
|
const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
974
1495
|
let packageManagerAgent;
|
|
975
1496
|
if (isCloudMode) {
|
|
976
|
-
// Force npm in cloud mode
|
|
1497
|
+
// Force npm in cloud mode; prepare before Mode 2 so dry-run malware
|
|
1498
|
+
// scan does not choke on workspace: protocol entries.
|
|
977
1499
|
packageManagerAgent = "npm";
|
|
978
1500
|
await this.maybePreparePackageJsonForCloudNpm();
|
|
979
1501
|
}
|
|
@@ -993,6 +1515,11 @@ let AppShell = (() => {
|
|
|
993
1515
|
}
|
|
994
1516
|
packageManagerAgent = packageManager.agent;
|
|
995
1517
|
}
|
|
1518
|
+
const appliedSubstitutions = await this.applyDeclaredDependencyUpgrades(await this.maybeRemediateVulnerabilities({
|
|
1519
|
+
abortSignal,
|
|
1520
|
+
mode: "all",
|
|
1521
|
+
}));
|
|
1522
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
996
1523
|
const isNpm = packageManagerAgent === "npm";
|
|
997
1524
|
const baseInstallArgs = isNpm
|
|
998
1525
|
? ["--fund=false", "--audit=false", "--json"]
|
|
@@ -1014,6 +1541,8 @@ let AppShell = (() => {
|
|
|
1014
1541
|
// default 1 MiB exec buffer on fat dep trees.
|
|
1015
1542
|
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
1016
1543
|
});
|
|
1544
|
+
// Audit only after the install succeeds (same as Mode 1).
|
|
1545
|
+
this.maybeReportSubstitutions(appliedSubstitutions, "install_all");
|
|
1017
1546
|
// On the npm path stdout is a JSON blob; translate to a compact
|
|
1018
1547
|
// human-readable summary for the agent, falling back to raw stdout
|
|
1019
1548
|
// when the JSON shape drifts.
|
|
@@ -1038,7 +1567,7 @@ let AppShell = (() => {
|
|
|
1038
1567
|
this.observeNpmInstall("install_all", capturedFetchResult, outcome, Date.now() - installStart);
|
|
1039
1568
|
}
|
|
1040
1569
|
}
|
|
1041
|
-
async installSpecificPackages(packages, abortSignal) {
|
|
1570
|
+
async installSpecificPackages(packages, abortSignal, options) {
|
|
1042
1571
|
if (!packages || packages.length === 0) {
|
|
1043
1572
|
throw new Error("No packages specified for installation");
|
|
1044
1573
|
}
|
|
@@ -1053,10 +1582,10 @@ let AppShell = (() => {
|
|
|
1053
1582
|
// surfaces them to the agent ("we tried to install lodash and the
|
|
1054
1583
|
// private registry was unreachable").
|
|
1055
1584
|
this.maybeShortCircuitForUnreachable(registryFetchResult, packages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name));
|
|
1056
|
-
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
1057
1585
|
// In cloud edit mode, always use npm because pnpm is not available in the container.
|
|
1058
1586
|
// The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
|
|
1059
|
-
// from local editing, but we must use npm in cloud mode.
|
|
1587
|
+
// from local editing, but we must use npm in cloud mode. Prepare before the
|
|
1588
|
+
// Mode 1 dry-run so leftover workspace: entries cannot break malware coverage.
|
|
1060
1589
|
const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
1061
1590
|
let packageManagerAgent;
|
|
1062
1591
|
if (isCloudMode) {
|
|
@@ -1079,10 +1608,21 @@ let AppShell = (() => {
|
|
|
1079
1608
|
}
|
|
1080
1609
|
packageManagerAgent = packageManager.agent;
|
|
1081
1610
|
}
|
|
1611
|
+
// Malware hard-block on the dry-run resolved tree (top-level +
|
|
1612
|
+
// transitive) before the real install runs. Mode 1 also returns
|
|
1613
|
+
// silent CVE substitutions applied to the install argv below.
|
|
1614
|
+
const remediation = await this.maybeRemediateVulnerabilities({
|
|
1615
|
+
abortSignal,
|
|
1616
|
+
cveUserOverride: options?.cveUserOverride,
|
|
1617
|
+
mode: "specific",
|
|
1618
|
+
packages,
|
|
1619
|
+
});
|
|
1620
|
+
const packagesToInstall = applyPackageSubstitutions(packages, remediation.substitutions);
|
|
1621
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
1082
1622
|
const isNpm = packageManagerAgent === "npm";
|
|
1083
1623
|
// Separate regular and dev dependencies
|
|
1084
|
-
const regularPackages =
|
|
1085
|
-
const devPackages =
|
|
1624
|
+
const regularPackages = packagesToInstall.filter((pkg) => !pkg.dev);
|
|
1625
|
+
const devPackages = packagesToInstall.filter((pkg) => pkg.dev);
|
|
1086
1626
|
const outputs = [];
|
|
1087
1627
|
// Install regular dependencies
|
|
1088
1628
|
if (regularPackages.length > 0) {
|
|
@@ -1146,6 +1686,18 @@ let AppShell = (() => {
|
|
|
1146
1686
|
throw new NpmInstallError(error.stderr || error.message);
|
|
1147
1687
|
}
|
|
1148
1688
|
}
|
|
1689
|
+
// Audit only after every install group succeeds: a selected
|
|
1690
|
+
// substitution is not an applied substitution if npm fails.
|
|
1691
|
+
this.maybeReportSubstitutions(remediation.substitutions, "install_specific");
|
|
1692
|
+
// Audited here, not at the tool layer: the gate is what consumed the
|
|
1693
|
+
// override, and the install has now actually landed.
|
|
1694
|
+
if (options?.cveUserOverride &&
|
|
1695
|
+
remediation.userOverridePackages?.length) {
|
|
1696
|
+
this.reportNpmUserOverride({
|
|
1697
|
+
packages: remediation.userOverridePackages,
|
|
1698
|
+
reason: options.cveUserOverride.reason,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1149
1701
|
return outputs.join("\n");
|
|
1150
1702
|
});
|
|
1151
1703
|
}
|