sentinelayer-cli 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1080,7 +1080,8 @@ Ledger contract:
|
|
|
1080
1080
|
|
|
1081
1081
|
## Requirements
|
|
1082
1082
|
|
|
1083
|
-
- Node
|
|
1083
|
+
- Node.js `^20.0.0 || ^22.0.0` (the package `engines`)
|
|
1084
|
+
- on Windows, Node.js 22 (libuv 1.48.0 or later): the CLI looks up the programs it runs (git, gh, python, npm and others) on `PATH` only, and an older runtime cannot do that, so on Windows the CLI exits with an error before running anything under Node.js 20
|
|
1084
1085
|
- network access to Sentinelayer API/web
|
|
1085
1086
|
- optional: GitHub CLI (`gh`) authenticated for secret injection
|
|
1086
1087
|
|
package/package.json
CHANGED
|
@@ -93,6 +93,9 @@ const EXACT_ENV_KEYS_TO_STRIP = new Set([
|
|
|
93
93
|
"MONGODB_URI",
|
|
94
94
|
"SSH_SIGNING_KEY",
|
|
95
95
|
"SSH_PRIVATE_KEY",
|
|
96
|
+
"SSH_AUTH_SOCK",
|
|
97
|
+
"PGPASSWORD",
|
|
98
|
+
"MYSQL_PWD",
|
|
96
99
|
"KUBECONFIG",
|
|
97
100
|
"KUBE_CONFIG_DATA",
|
|
98
101
|
]);
|
|
@@ -114,6 +117,7 @@ const ENV_KEY_PREFIX_PATTERNS = [
|
|
|
114
117
|
/^TWILIO_/i,
|
|
115
118
|
/^SENDGRID_/i,
|
|
116
119
|
/^RESEND_/i,
|
|
120
|
+
/^NPM_CONFIG_.*_AUTH(TOKEN)?$/i, // npm's _auth and _authToken, global or per registry
|
|
117
121
|
];
|
|
118
122
|
|
|
119
123
|
const ENV_KEY_SUFFIX_PATTERNS = [
|
|
@@ -126,6 +130,11 @@ const ENV_KEY_SUFFIX_PATTERNS = [
|
|
|
126
130
|
/_ACCESS_KEY$/i,
|
|
127
131
|
/_AUTH_TOKEN$/i,
|
|
128
132
|
/_SESSION_TOKEN$/i,
|
|
133
|
+
/_KEY$/i,
|
|
134
|
+
/_DSN$/i,
|
|
135
|
+
/_PASS$/i,
|
|
136
|
+
/_CREDENTIALS?$/i,
|
|
137
|
+
/_CONNECTION_STRING$/i,
|
|
129
138
|
];
|
|
130
139
|
|
|
131
140
|
/**
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
|
|
5
|
+
import { restrictProgramLookupToPath } from "./program-lookup.js";
|
|
5
6
|
|
|
6
7
|
const COMMAND_REGISTRARS = {
|
|
7
8
|
init: {
|
|
@@ -242,6 +243,15 @@ export async function buildCliProgram({
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
246
|
+
// Programs named without a path come from PATH only, never from the working directory, before
|
|
247
|
+
// anything is spawned. On a Windows runtime that cannot guarantee that, nothing runs.
|
|
248
|
+
const programLookup = restrictProgramLookupToPath();
|
|
249
|
+
if (!programLookup.ok) {
|
|
250
|
+
console.error(programLookup.message);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
245
255
|
// Where the user's credentials may go is decided once, here, from the real environment and the
|
|
246
256
|
// global config, and frozen: nothing that changes process.env later can move it.
|
|
247
257
|
const { freezeProcessTrustContext } = await import("./auth/credential-destinations.js");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { extname, resolve as resolvePath } from "node:path";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
5
|
|
|
5
6
|
import { parse } from "@babel/parser";
|
|
@@ -149,7 +150,7 @@ async function resolvePythonExecutable() {
|
|
|
149
150
|
pythonExecutablePromise = (async () => {
|
|
150
151
|
for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
|
|
151
152
|
try {
|
|
152
|
-
await execFileAsync(executable, ["--version"], { timeout: 5000 });
|
|
153
|
+
await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
|
|
153
154
|
return executable;
|
|
154
155
|
} catch {
|
|
155
156
|
continue;
|
|
@@ -193,9 +194,13 @@ async function parsePythonAstSpecifiers(absolutePath = "") {
|
|
|
193
194
|
};
|
|
194
195
|
}
|
|
195
196
|
try {
|
|
196
|
-
|
|
197
|
+
// Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
|
|
198
|
+
// a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
|
|
199
|
+
// parse is passed as an argument.
|
|
200
|
+
const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_AST_SCRIPT, resolvePath(absolutePath)], {
|
|
197
201
|
timeout: 10000,
|
|
198
202
|
maxBuffer: 1024 * 1024,
|
|
203
|
+
cwd: os.tmpdir(),
|
|
199
204
|
});
|
|
200
205
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
201
206
|
const specifiers = Array.isArray(parsed.specifiers) ? parsed.specifiers : [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path, { extname } from "node:path";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
|
|
@@ -370,7 +371,7 @@ async function resolvePythonExecutable() {
|
|
|
370
371
|
pythonExecutablePromise = (async () => {
|
|
371
372
|
for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
|
|
372
373
|
try {
|
|
373
|
-
await execFileAsync(executable, ["--version"], { timeout: 5000 });
|
|
374
|
+
await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
|
|
374
375
|
return executable;
|
|
375
376
|
} catch {
|
|
376
377
|
continue;
|
|
@@ -454,9 +455,13 @@ async function parsePythonCallgraph(absolutePath = "", content = "", normalizedL
|
|
|
454
455
|
};
|
|
455
456
|
}
|
|
456
457
|
try {
|
|
457
|
-
|
|
458
|
+
// Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
|
|
459
|
+
// a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
|
|
460
|
+
// parse is passed as an argument.
|
|
461
|
+
const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_CALLGRAPH_SCRIPT, path.resolve(absolutePath)], {
|
|
458
462
|
timeout: 15000,
|
|
459
463
|
maxBuffer: 1024 * 1024,
|
|
464
|
+
cwd: os.tmpdir(),
|
|
460
465
|
});
|
|
461
466
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
462
467
|
return {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
// Windows looks up a program named without a path in the working directory before PATH. The CLI
|
|
4
|
+
// runs git, gh, python, npm and other tools by name, often from inside a repository, so it turns
|
|
5
|
+
// that lookup off with NoDefaultCurrentDirectoryInExePath. Node.js spawns through libuv, which
|
|
6
|
+
// honours that variable from libuv 1.48.0 (Node.js 22); an older libuv ignores it. On such a
|
|
7
|
+
// runtime the CLI does not run at all on Windows, rather than run with the lookup on.
|
|
8
|
+
export const MIN_WINDOWS_LIBUV = Object.freeze([1, 48, 0]);
|
|
9
|
+
|
|
10
|
+
function parseVersion(value) {
|
|
11
|
+
const parts = String(value || "").split(".");
|
|
12
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const numbers = parts.map((part) => (/^\d+$/.test(part) ? Number(part) : NaN));
|
|
16
|
+
return numbers.every(Number.isInteger) ? [...numbers, 0].slice(0, 3) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function atLeast(version, minimum) {
|
|
20
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
21
|
+
if (version[index] !== minimum[index]) {
|
|
22
|
+
return version[index] > minimum[index];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Turn off working-directory program lookup for this process and the children that inherit its
|
|
30
|
+
* environment. Returns { ok: false, message } instead when this is Windows and the runtime would
|
|
31
|
+
* ignore the setting.
|
|
32
|
+
*/
|
|
33
|
+
export function restrictProgramLookupToPath({
|
|
34
|
+
platform = process.platform,
|
|
35
|
+
libuv = process.versions.uv,
|
|
36
|
+
nodeVersion = process.version,
|
|
37
|
+
env = process.env,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (platform !== "win32") {
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
const version = parseVersion(libuv);
|
|
43
|
+
if (!version || !atLeast(version, MIN_WINDOWS_LIBUV)) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
message:
|
|
47
|
+
`On Windows, sentinelayer-cli needs Node.js 22 or later (libuv ${MIN_WINDOWS_LIBUV.join(".")} or later). ` +
|
|
48
|
+
`This is Node.js ${nodeVersion} with libuv ${libuv || "unknown"}, which looks up programs in the current ` +
|
|
49
|
+
"directory before PATH. Upgrade Node.js and run the command again.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
env.NoDefaultCurrentDirectoryInExePath = "1";
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import fsp from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
import { buildScrubbedEnv } from "../agents/shared-tools/shell.js";
|
|
6
7
|
import { resolveOutputRoot } from "../config/service.js";
|
|
7
8
|
import { resolveCodebaseIngest } from "../ingest/engine.js";
|
|
8
9
|
import { runSpecBindingChecks } from "./spec-binding.js";
|
|
@@ -844,6 +845,13 @@ function buildStaticChecks(ingest = {}) {
|
|
|
844
845
|
];
|
|
845
846
|
}
|
|
846
847
|
|
|
848
|
+
// The repository's own scripts run here (lint, typecheck, format check, tests). Running its code is
|
|
849
|
+
// the point; handing it this machine's credentials is not. SentinelLayer, provider, GitHub, cloud
|
|
850
|
+
// and other secret-looking variables are removed from its environment.
|
|
851
|
+
export function staticCheckEnv(env = process.env) {
|
|
852
|
+
return { ...buildScrubbedEnv(env), CI: "1", FORCE_COLOR: "0" };
|
|
853
|
+
}
|
|
854
|
+
|
|
847
855
|
async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
848
856
|
const checksDir = path.join(runDir, "checks");
|
|
849
857
|
await fsp.mkdir(checksDir, { recursive: true });
|
|
@@ -853,11 +861,7 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
|
853
861
|
cwd: targetPath,
|
|
854
862
|
encoding: "utf-8",
|
|
855
863
|
timeout: STATIC_CHECK_TIMEOUT_MS,
|
|
856
|
-
env:
|
|
857
|
-
...process.env,
|
|
858
|
-
CI: "1",
|
|
859
|
-
FORCE_COLOR: "0",
|
|
860
|
-
},
|
|
864
|
+
env: staticCheckEnv(),
|
|
861
865
|
});
|
|
862
866
|
|
|
863
867
|
const durationMs = Date.now() - startedAt;
|
|
@@ -927,6 +931,8 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
|
927
931
|
};
|
|
928
932
|
}
|
|
929
933
|
|
|
934
|
+
export const __localReviewForTests = Object.freeze({ executeStaticCheck });
|
|
935
|
+
|
|
930
936
|
async function runStaticAnalysisLayer({ targetPath, ingest, runDir, maxFindings = MAX_FINDINGS } = {}) {
|
|
931
937
|
const checks = buildStaticChecks(ingest);
|
|
932
938
|
const results = [];
|