CH-J Toolsserverhåndtering over SSH
Menu
Offentliggjort kilde

CH-J Tools

Gennemse mapper og filer til en specifik applikationsudgivelse.

Download kilde ZIP
CH-J / Server Manager 406 filer
core-20261024T135205Z-7c487d719de2 Tilføjet: · Bygget:
usersWindows.test.js Tilføjet: Dato ikke registreret 26,6 KB · 324 linjer
Kode Download fil
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const windowsPolicy = require("../src/main/users/windowsSshPolicy");
6const { normalizePolicy, parseEffective, evaluateAccess } = require("../src/main/users/sshPolicy");
7const { BOOTSTRAP, bootstrapInput, encodedCommand, parseWindowsOutput } = require("../src/main/sessions/windowsPrivilegedExec");
8const { detectPlatform, shellName } = require("../src/main/sessions/platformDetect");
9const { WindowsUserAdminService, privilegeFromToken, validateWindowsName } = require("../src/main/users/windowsUserAdminService");
10const { UserAdminService } = require("../src/main/users/userAdminService");
12// Default sshd_config of Windows OpenSSH 9.5 (comments shortened), CRLF line endings as shipped.
13const WINDOWS_CONFIG = [
14 "# This is the sshd server system-wide configuration file.",
15 "#PasswordAuthentication yes",
16 "AuthorizedKeysFile\t.ssh/authorized_keys",
17 "Subsystem\tsftp\tsftp-server.exe",
18 "",
19 "Match Group administrators",
20 " AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys",
21 ""
22].join("\r\n");
23const ok = (value) => `#< CLIXML\r\nCHJ_RESULT:${Buffer.from(JSON.stringify(value)).toString("base64")}\r\n`;
24const err = (code, message) => `CHJ_ERR:${code}:${Buffer.from(message).toString("base64")}\r\n`;
26test("Windows sshd_config sections: DenyUsers before the first Match, Match User at the end, no KbdInteractive", () => {
27 const policies = new Map([
28 ["alice", normalizePolicy({ password: "no", ipv6: "deny" })],
29 ["bob", normalizePolicy({ access: "deny" })],
30 ["carol", normalizePolicy({})]
31 ]);
32 const text = windowsPolicy.renderConfig(WINDOWS_CONFIG, policies);
33 const lines = text.split("\r\n");
34 const globalIndex = lines.indexOf(windowsPolicy.GLOBAL_BEGIN);
35 const adminMatch = lines.indexOf("Match Group administrators");
36 assert.ok(globalIndex >= 0 && globalIndex < adminMatch, "global directives never follow an existing Match block");
37 assert.ok(lines.indexOf("DenyUsers alice@*:*") > globalIndex && lines.indexOf("DenyUsers alice@*:*") < adminMatch, "IPv6 rule without CIDR slash");
38 assert.doesNotMatch(text, /DenyUsers [^\r\n]*\//, "no slash: Windows OpenSSH rewrites it to a backslash");
39 assert.ok(lines.indexOf("DenyUsers bob") < adminMatch);
40 assert.ok(lines.indexOf("Match User alice") > adminMatch, "per-user Match blocks keep the lower priority");
41 assert.doesNotMatch(text, /KbdInteractiveAuthentication/);
42 assert.doesNotMatch(text, /carol/);
43 assert.doesNotMatch(text, /[^\r]\n/, "CRLF line endings are preserved");
44 const parsed = windowsPolicy.parseManagedPolicies(text);
45 assert.equal(parsed.consistent, true);
46 assert.deepEqual([...parsed.policies.keys()].sort(), ["alice", "bob"]);
47 assert.equal(windowsPolicy.renderConfig(text, new Map()), WINDOWS_CONFIG, "removing all policies restores the original bytes");
48 assert.equal(windowsPolicy.renderConfig(WINDOWS_CONFIG, new Map()), WINDOWS_CONFIG);
49});
51test("Windows sshd_config edits are refused for manual edits, damaged markers, Include and misplaced sections", () => {
52 const text = windowsPolicy.renderConfig(WINDOWS_CONFIG, new Map([["alice", normalizePolicy({ ipv4: "deny" })]]));
53 const edited = text.replace("DenyUsers alice@*.*.*.*", "DenyUsers alice@*.*.*.* mallory");
54 assert.equal(windowsPolicy.parseManagedPolicies(edited).consistent, false);
55 assert.throws(() => windowsPolicy.renderConfig(edited, new Map()), { code: "USERS_SSH_MANAGED_FILE_MODIFIED" });
56 assert.throws(() => windowsPolicy.renderConfig(text.replace(windowsPolicy.GLOBAL_BEGIN, "# x"), new Map()), { code: "USERS_SSH_LAYOUT_UNSUPPORTED" });
57 assert.throws(() => windowsPolicy.renderConfig(`Include C:/ProgramData/ssh/extra.conf\r\n${WINDOWS_CONFIG}`, new Map([["alice", normalizePolicy({ access: "deny" })]])), { code: "USERS_SSH_LAYOUT_UNSUPPORTED" });
58 const misplaced = `${WINDOWS_CONFIG}${windowsPolicy.GLOBAL_BEGIN}\r\n# END CH-J chj.users global\r\n`;
59 assert.equal(windowsPolicy.analyzeConfig(misplaced).supported, false);
60 assert.equal(windowsPolicy.renderConfig("PasswordAuthentication yes\n", new Map([["alice", normalizePolicy({ password: "no" })]])),
61 `PasswordAuthentication yes\n${windowsPolicy.GLOBAL_BEGIN}\n# chj-policy alice access=allow password=no pubkey=inherit ipv4=allow ipv6=allow\n# END CH-J chj.users global\n${windowsPolicy.MATCH_BEGIN}\nMatch User alice\n\tPasswordAuthentication no\n# END CH-J chj.users match\n`);
62 assert.equal(windowsPolicy.sshName("Codex"), "codex");
63 assert.equal(windowsPolicy.sshName("Jan Novák"), null, "names with spaces or non-ASCII are not written to sshd_config");
64});
66test("Windows effective evaluation ignores keyboard-interactive, which Windows OpenSSH does not support", () => {
67 const dump = parseEffective("passwordauthentication no\nkbdinteractiveauthentication yes\npubkeyauthentication yes\ndenyusers alice@*.*.*.*\ndenyusers bob@*:*");
68 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", method: "password", keyboardInteractive: false }).methodAllowed, false);
69 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", method: "password" }).methodAllowed, true, "Linux semantics unchanged");
70 assert.equal(evaluateAccess(dump, { username: "alice", address: "192.0.2.1", keyboardInteractive: false }).allowed, false);
71 assert.equal(evaluateAccess(dump, { username: "alice", address: "2001:db8::1", keyboardInteractive: false }).allowed, true);
72 assert.equal(evaluateAccess(dump, { username: "bob", address: "2001:db8::1", keyboardInteractive: false }).allowed, false);
73 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", keyboardInteractive: false }).reasons.length, 0);
74 assert.equal(evaluateAccess(dump, { username: "alice", address: "192.0.2.1" }).reasons[0].managed, true);
75});
77test("PowerShell transport: short encoded bootstrap, structured stdin, no user data in the command line", () => {
78 const command = encodedCommand(BOOTSTRAP);
79 assert.ok(command.length < 8191, "fits the cmd.exe command-line limit");
80 assert.match(command, /^powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand [A-Za-z0-9+/=]+$/);
81 assert.match(Buffer.from(command.split(" ").pop(), "base64").toString("utf16le"), /\[Console\]::In/);
82 const input = bootstrapInput("script", { op: "account", password: "S3cret;& del *", name: "a\"b" });
83 const [script, args] = input.trim().split("\n").map((line) => Buffer.from(line, "base64").toString("utf8"));
84 assert.equal(script, "script");
85 assert.deepEqual(JSON.parse(args), { op: "account", password: "S3cret;& del *", name: "a\"b" });
86 assert.equal(command.includes("S3cret"), false);
87});
89test("PowerShell output parsing: results, error codes, cmd.exe failures and restricted PowerShell", () => {
90 assert.deepEqual(parseWindowsOutput(ok({ users: [{ name: "Jiří" }] }), "<Objs>progress</Objs>", 0), { users: [{ name: "Jiří" }] });
91 assert.throws(() => parseWindowsOutput(err("WINDOWS_ELEVATION_REQUIRED", "Token není zvýšený"), "", 0), { code: "USERS_WINDOWS_ELEVATION_REQUIRED", message: "Token není zvýšený" });
92 assert.throws(() => parseWindowsOutput("", "'powershell.exe' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n", 1), { code: "USERS_POWERSHELL_UNAVAILABLE" });
93 assert.throws(() => parseWindowsOutput("", "Cannot invoke method. Method invocation is supported only on core types in this language mode.", 1), { code: "USERS_POWERSHELL_RESTRICTED" });
94 assert.throws(() => parseWindowsOutput("garbage", "", 0), { code: "USERS_PROTOCOL_ERROR" });
95 assert.throws(() => parseWindowsOutput("CHJ_RESULT:not-base64-json", "", 0), { code: "USERS_PROTOCOL_ERROR" });
96});
98function fakeManager(responses) {
99 const manager = { calls: [], record: { sessionId: "s", platform: null } };
100 manager._requireConnected = () => manager.record;
101 manager._windowsSerial = (_record, task) => task();
102 manager._execStream = (_record, command) => {
103 manager.calls.push(command);
104 const response = responses.shift();
105 return { done: response instanceof Error ? Promise.reject(response) : Promise.resolve({ code: 0, stdout: "", stderr: "", ...response }) };
106 };
107 return manager;
110test("platform detection: uname for POSIX, Win32_OperatingSystem for Windows (cmd.exe, Git Bash uname), unknown otherwise", async () => {
111 let manager = fakeManager([{ code: 0, stdout: "Linux\n" }]);
112 assert.equal((await detectPlatform(manager, "s")).os, "linux");
113 assert.equal(manager.calls.length, 1);
114 const windowsInfo = { caption: "Microsoft Windows Server 2025 Datacenter", version: "10.0.26100", build: "26100", productType: 3, installationType: "Server Core", defaultShell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", psVersion: "5.1.26100.1", is64BitProcess: true, languageMode: "FullLanguage", localAccounts: true, pwsh: "" };
115 manager = fakeManager([{ code: 1, stderr: "'uname' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n" }, { stdout: ok(windowsInfo) }]);
116 const server = await detectPlatform(manager, "s");
117 assert.equal(server.os, "windows"); assert.equal(server.serverCore, true); assert.equal(server.server, true); assert.equal(server.defaultShell, "powershell");
118 assert.match(manager.calls[1], /-EncodedCommand /);
119 manager = fakeManager([{ code: 0, stdout: "MINGW64_NT-10.0-26200\n" }, { stdout: ok({ ...windowsInfo, productType: 1, installationType: "Client", defaultShell: "" }) }]);
120 const client = await detectPlatform(manager, "s");
121 assert.equal(client.os, "windows", "Git for Windows uname does not make the host Linux"); assert.equal(client.defaultShell, "cmd.exe");
122 manager = fakeManager([{ code: 127, stderr: "sh: uname: not found" }, { code: 127, stderr: "sh: powershell.exe: not found" }]);
123 assert.equal((await detectPlatform(manager, "s")).os, "unknown");
124 assert.equal(shellName("C:\\Program Files\\PowerShell\\7\\pwsh.exe"), "pwsh");
125 assert.equal(shellName("C:\\Windows\\System32\\cmd.exe"), "cmd.exe");
126});
128test("Windows token states: elevated, Administrators member with filtered token, standard user", () => {
129 assert.equal(privilegeFromToken({ elevated: true, adminMember: true, name: "PC\\admin", sid: "S-1-5-21-1-2-3-1001" }, { sshClient: "192.0.2.7 50000 22" }).mode, "windows-elevated");
130 const filtered = privilegeFromToken({ elevated: false, adminMember: true, adminDenyOnly: true, name: "PC\\admin" }, { sshClient: "2001:db8::7 50000 22" });
131 assert.equal(filtered.mode, "windows-admin-unelevated");
132 assert.equal(filtered.clientAddress, "2001:db8::7");
133 assert.equal(privilegeFromToken({ elevated: false, adminMember: false }, {}).mode, "windows-standard");
134 assert.equal(filtered.sudoInstalled, false);
135});
137const MACHINE = "S-1-5-21-3493863762-3532507802-4093449623";
138function inventoryFixture({ elevated = true } = {}) {
139 const user = (name, rid, extra = {}) => ({ name, sid: `${MACHINE}-${rid}`, enabled: true, fullName: "", description: "", source: "Local", lastLogon: null, passwordLastSet: "2026-10-01T10:00:00.0000000Z", passwordRequired: true, passwordExpires: null, accountExpires: null, userMayChangePassword: true, ...extra });
140 return {
141 system: { caption: "Microsoft Windows 11 Pro", version: "10.0.26200", displayVersion: "25H2", installationType: "Client", productType: 1, partOfDomain: true, domain: "corp.example", psVersion: "5.1.26100.9711", psEdition: "Desktop", is64BitProcess: true, sshClient: "192.0.2.142 51000 22" },
142 token: { name: "WIN-TEST\\admin", sid: `${MACHINE}-1002`, elevated, adminMember: true, integrity: elevated ? "high" : "medium" },
143 users: [user("Administrator", 500, { enabled: false }), user("Guest", 501, { enabled: false }), user("codex", 1002), user("novak", 1004, { fullName: "Jan Novák" }), user("yosef", 1001, { source: "MicrosoftAccount" })],
144 // Localized (Czech) group names: Administrators must be identified by SID only.
145 groups: [
146 { sid: "S-1-5-32-544", name: "Administrátoři", members: [{ sid: `${MACHINE}-500`, name: "PC\\Administrator" }, { sid: `${MACHINE}-1002`, name: "PC\\codex" }, { sid: "S-1-5-21-111-222-333-1105", name: "CORP\\ops", source: "ActiveDirectory" }] },
147 { sid: "S-1-5-32-545", name: "Uživatelé", members: [{ sid: `${MACHINE}-1004`, name: "PC\\novak" }, { sid: `${MACHINE}-1002`, name: "PC\\codex" }] },
148 { sid: "S-1-5-32-555", name: "Uživatelé vzdálené plochy", members: [] }
149 ],
150 ssh: { installed: true, version: "OpenSSH_for_Windows_9.5p2", service: "Running", configReadable: true, configSha256: "abc", configData: Buffer.from(WINDOWS_CONFIG).toString("base64") }
151 };
154function service(runs, { alive = true } = {}) {
155 const calls = [];
156 const executor = { run: async (_sessionId, { args }) => { calls.push(args); const next = runs[args.op]; if (typeof next === "function") return next(args); if (next instanceof Error) throw next; return next; } };
157 const sessionManager = {
158 sessionAuthInfo: () => ({ username: "codex", authMethod: "password", publicKeyFingerprint: null }),
159 probeLoginRequirements: (_id, credentials) => credentials.password ? { ready: true } : { ready: false, missing: "password" },
160 connectionTarget: () => ({ profileId: "p", address: "192.0.2.143" }),
161 _requireConnected: () => { if (!alive) throw Object.assign(new Error("gone"), { code: "SESSION_NOT_CONNECTED" }); return {}; },
162 freshExec: async () => { throw Object.assign(new Error("A new SSH login was not confirmed: auth failed"), { code: "SSH_PROBE_FAILED" }); }
163 };
164 return { svc: new WindowsUserAdminService({ sessionManager, windowsExecutor: executor }), calls, sessionManager };
167test("Windows inventory: SID identity, Administrators by SID with localized names, protected and external identities", async () => {
168 const { svc } = service({ inventory: inventoryFixture() });
169 const inventory = await svc.inventory("s", { defaultShell: "cmd.exe" });
170 const byName = Object.fromEntries(inventory.users.map((user) => [user.username, user]));
171 assert.equal(inventory.platform, "windows");
172 assert.equal(byName.codex.admin, true);
173 assert.deepEqual(byName.codex.groups.sort(), ["Administrátoři", "Uživatelé"].sort());
174 assert.equal(byName.novak.admin, false);
175 assert.equal(byName.Administrator.protection, "builtin");
176 assert.equal(byName.codex.protection, "session");
177 assert.equal(byName.novak.protection, null);
178 assert.equal(byName.novak.uid, undefined, "no invented Linux UID");
179 assert.equal(byName.novak.sudo, undefined, "Windows administrators are not presented as sudo");
180 assert.equal(inventory.externalIdentities.length, 1);
181 assert.equal(inventory.externalIdentities[0].name, "CORP\\ops");
182 assert.equal(inventory.server.partOfDomain, true);
183 assert.equal(inventory.privileged, true);
184 assert.equal(inventory.server.sshLayout, "ok");
185});
187test("Windows management refuses without an elevated token, for protected accounts, Linux-only actions and bad input", async () => {
188 const unelevated = service({ probe: { token: inventoryFixture().token && { ...inventoryFixture({ elevated: false }).token }, system: inventoryFixture().system } });
189 await assert.rejects(() => unelevated.svc.manage("s", { action: "disable", sid: `${MACHINE}-1004` }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED", message: /UAC/ });
190 assert.deepEqual(unelevated.calls.map((call) => call.op), ["probe"], "nothing is attempted without elevation");
191 const elevated = service({ probe: { token: inventoryFixture().token, system: inventoryFixture().system }, inventory: inventoryFixture(), account: { result: "ok", steps: [] } });
192 await assert.rejects(() => elevated.svc.manage("s", { action: "disable", sid: `${MACHINE}-500` }), { code: "USERS_PROTECTED_USER" });
193 await assert.rejects(() => elevated.svc.manage("s", { action: "delete", sid: `${MACHINE}-1002` }), { code: "USERS_PROTECTED_USER" });
194 await assert.rejects(() => elevated.svc.manage("s", { action: "setShell", sid: `${MACHINE}-1004`, shell: "/bin/sh" }), { code: "USERS_NOT_SUPPORTED_ON_PLATFORM" });
195 await assert.rejects(() => elevated.svc.manage("s", { action: "disable", sid: "S-1-5-21-1-2-3-4;Remove-LocalUser" }), { code: "USERS_SID_INVALID" });
196 await assert.rejects(() => elevated.svc.manage("s", { action: "addGroup", sid: `${MACHINE}-1004`, group: "Administrators" }), { code: "USERS_GROUP_INVALID" });
197 await assert.rejects(() => elevated.svc.manage("s", { action: "setPassword", sid: `${MACHINE}-1004`, password: "a\r\nb" }), { code: "USERS_PASSWORD_INVALID" });
198 await elevated.svc.manage("s", { action: "grantAdmin", sid: `${MACHINE}-1004` });
199 const call = elevated.calls.find((entry) => entry.op === "account");
200 assert.deepEqual(call, { op: "account", action: "admin-grant", sid: `${MACHINE}-1004` });
201 for (const name of ["a;b", "x$(y)", "a|b", "a\"b", "bad/name", "toolong_abcdefghijklm", "end.", "mid\nline"]) assert.throws(() => validateWindowsName(name), { code: "USERS_NAME_INVALID" }, name);
202 assert.equal(validateWindowsName("Jan Novak"), "Jan Novak");
203});
205function detailFixture(name = "novak", rid = 1004, groups = [{ sid: "S-1-5-32-545", name: "Uživatelé" }]) {
206 const effective = (extra = "") => ({ code: 0, text: `passwordauthentication yes\nkbdinteractiveauthentication yes\npubkeyauthentication yes\nauthorizedkeysfile .ssh/authorized_keys\nlistenaddress 0.0.0.0:22\nlistenaddress [::]:22${extra}` });
207 return {
208 user: { name, sid: `${MACHINE}-${rid}`, enabled: true, fullName: "", description: "", source: "Local" },
209 groups, profilePath: `C:\\Users\\${name}`, token: inventoryFixture().token, system: inventoryFixture().system,
210 ssh: inventoryFixture().ssh, syntax: { code: 0, output: "" }, effective4: effective(), effective6: effective(),
211 keyFiles: [{ spec: ".ssh/authorized_keys", path: `C:\\Users\\${name}\\.ssh\\authorized_keys`, shared: false, type: "absent", dirExists: true, reparse: false, acl: [] }]
212 };
215test("Windows detail: effective IPv4/IPv6 state, shared administrators key file and unsafe ACL warnings", async () => {
216 const admin = detailFixture("codex", 1002, [{ sid: "S-1-5-32-544", name: "Administrátoři" }]);
217 admin.keyFiles = [{ spec: "__PROGRAMDATA__/ssh/administrators_authorized_keys", path: "C:\\ProgramData\\ssh\\administrators_authorized_keys", shared: true, type: "file", reparse: false, sha256: "x", data: "", acl: [{ sid: "S-1-5-18", rights: "FullControl", type: "Allow" }, { sid: "S-1-5-32-544", rights: "FullControl", type: "Allow" }, { sid: "S-1-5-11", rights: "Modify, Synchronize", type: "Allow" }] }];
218 const { svc } = service({ probe: { token: inventoryFixture().token, system: inventoryFixture().system }, detail: admin });
219 const detail = await svc.detail("s", { sid: `${MACHINE}-1002` }, {});
220 assert.equal(detail.account.admin, true);
221 assert.equal(detail.self, true);
222 assert.equal(detail.keys.files[0].shared, true);
223 assert.ok(detail.ssh.warnings.some((warning) => /share one administrators_authorized_keys/.test(warning)));
224 assert.ok(detail.ssh.warnings.some((warning) => /S-1-5-11 has Modify/.test(warning)));
225 assert.equal(detail.ssh.effective.ipv4.methods.password, true);
226 assert.equal(detail.ssh.layoutSupported, true);
227 assert.equal(Object.keys(detail).some((key) => key.startsWith("_") && key !== "_keyFiles" && key !== "_config" && key !== "_addresses" && key !== "_groups"), false);
228});
230test("Windows SSH policy: existing Match precedence, lockout and missing test-login credentials stop before any change", async () => {
231 const stageWith = (afterExtra) => (args) => ({
232 id: "a".repeat(32), syntaxCode: 0, syntaxOutput: "", candidateSha256: "c",
233 before: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: "passwordauthentication yes\npubkeyauthentication yes" }])),
234 after: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: afterExtra(tuple) }]))
235 });
236 const base = { probe: { token: inventoryFixture().token, system: inventoryFixture().system }, detail: detailFixture() };
237 let fixture = service({ ...base, "ssh-stage": stageWith(() => "passwordauthentication yes\npubkeyauthentication yes") });
238 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "x" } }), { code: "USERS_SSH_POLICY_NOT_EFFECTIVE" });
239 assert.equal(fixture.calls.some((call) => call.op === "ssh-commit"), false, "nothing is committed");
240 fixture = service(base);
241 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" } }), { code: "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" });
242 assert.equal(fixture.calls.some((call) => call.op === "ssh-stage"), false);
243 fixture = service({ ...base, "ssh-stage": stageWith((tuple) => tuple.user === "codex" ? "passwordauthentication yes\npubkeyauthentication yes\ndenyusers codex@0.0.0.0/0" : "passwordauthentication no\npubkeyauthentication yes") });
244 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "x" } }), { code: "USERS_SSH_LOCKOUT_PREVENTED" });
245 assert.equal(fixture.calls.some((call) => call.op === "ssh-commit"), false);
246});
248test("Windows SSH policy: a failing new login never confirms and the server-side guard rolls back", async () => {
249 const fixture = service({
250 probe: { token: inventoryFixture().token, system: inventoryFixture().system },
251 detail: detailFixture(),
252 "ssh-stage": (args) => ({ id: "b".repeat(32), syntaxCode: 0, syntaxOutput: "", candidateSha256: "c",
253 before: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: "passwordauthentication yes\npubkeyauthentication yes" }])),
254 after: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: tuple.user === "novak" ? "passwordauthentication no\npubkeyauthentication yes" : "passwordauthentication yes\npubkeyauthentication yes" }])) }),
255 "ssh-commit": { id: "b".repeat(32), state: "awaiting-confirmation", message: "sshd restarted" },
256 "ssh-confirm": (args) => ({ id: args.id, state: args.confirm ? "confirmed" : "rolled-back", message: "No confirmation from a new SSH login before the timeout." })
257 });
258 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "wrong" } }), (error) => {
259 assert.equal(error.code, "USERS_SSH_PROBE_FAILED");
260 assert.match(error.message, /rolled-back/);
261 return true;
262 });
263 const ops = fixture.calls.map((call) => call.op);
264 assert.deepEqual(ops.slice(-3), ["ssh-stage", "ssh-commit", "ssh-confirm"]);
265 assert.equal(fixture.calls.at(-1).confirm, false, "the old session only waits for the guard and never confirms");
266 assert.match(fixture.calls.find((call) => call.op === "ssh-commit").nonce, /^[0-9a-f]{48}$/);
267});
269test("the shared users service routes Windows sessions to the Windows backend without touching sudo", async () => {
270 const calls = [];
271 const windowsService = new Proxy({}, { get: (_target, name) => async (...args) => { calls.push(name); return { platform: "windows", args: args.length }; } });
272 const linuxExecutor = { probe: async () => { throw new Error("sudo probe must not run on Windows"); } };
273 const sessionManager = { _requireConnected: () => ({}) };
274 const shared = new UserAdminService({ sessionManager, privilegedExecutor: linuxExecutor, windowsService, detectPlatform: async () => ({ os: "windows", defaultShell: "cmd.exe" }) });
275 for (const method of ["privileges", "inventory", "detail", "manage", "keys", "sshPolicy"]) await shared[method]("s", {});
276 assert.deepEqual(calls, ["privileges", "inventory", "detail", "manage", "keys", "sshPolicy"]);
277 const unknown = new UserAdminService({ sessionManager, privilegedExecutor: linuxExecutor, windowsService, detectPlatform: async () => ({ os: "unknown", reason: "no shell" }) });
278 await assert.rejects(() => unknown.inventory("s"), { code: "USERS_PLATFORM_UNSUPPORTED" });
279});
281test("Windows remote script keeps secrets in stdin, identifies Administrators by SID and arms rollback before restart", () => {
282 const script = require("node:fs").readFileSync(require("node:path").join(__dirname, "../src/main/users/windows/remoteUsers.ps1"), "utf8");
283 assert.match(script, /\$AdminsSid = 'S-1-5-32-544'/);
284 assert.doesNotMatch(script, /-Name '?Administrators'?|Get-LocalGroup -Name Administrators/i, "no localized group names");
285 assert.doesNotMatch(script, /KbdInteractiveAuthentication/);
286 assert.doesNotMatch(script, /\bsudo\b/);
287 assert.doesNotMatch(script, /Invoke-Expression|\biex\b/);
288 assert.match(script, /\[NullString\]::Value/, "File.Replace receives a real null backup path");
289 const apply = script.slice(script.indexOf("$ApplyScript = @'"));
290 assert.ok(apply.indexOf("Status 'armed'") < apply.indexOf("RestartSshd\n") || apply.indexOf("Status 'armed'") < apply.indexOf("try { RestartSshd }"), "rollback guard is armed before the restart");
291 assert.match(apply, /Restore 'No confirmation from a new SSH login before the timeout.'/);
292});
294test("Windows helpers of one session run one after another, also after a failure (Win32-OpenSSH stalls on concurrent stdin helpers)", async () => {
295 const { SessionManager } = require("../src/main/sessions/sessionManager");
296 const { WindowsExecutor } = require("../src/main/sessions/windowsPrivilegedExec");
297 const manager = new SessionManager({ latencyMonitor: { start: () => () => {} }, profileService: {} });
298 const record = { sessionId: "w", state: "connected" };
299 manager._requireConnected = () => record;
300 let active = 0; let peak = 0; const order = [];
301 manager._execStream = (_record, _command, { onUpdate }) => {
302 active += 1; peak = Math.max(peak, active);
303 const controller = { stdout: "", stderr: "", write: (text) => order.push(JSON.parse(Buffer.from(text.split("\n")[1], "base64").toString("utf8")).op), end() {} };
304 onUpdate(controller, "open");
305 const op = order[order.length - 1];
306 const done = new Promise((resolve, reject) => setTimeout(() => {
307 active -= 1;
308 if (op === "boom") reject(Object.assign(new Error("remote failure"), { code: "REMOTE_COMMAND_FAILED" }));
309 else resolve({ code: 0, stderr: "", stdout: ok({ op }) });
310 }, 15));
311 return { done };
312 };
313 const executor = new WindowsExecutor({ sessionManager: manager });
314 const results = await Promise.allSettled([
315 executor.run("w", { script: "s", args: { op: "list" } }),
316 executor.run("w", { script: "s", args: { op: "boom" } }),
317 manager._runWindowsScript(record, "s", { op: "metrics" }),
318 executor.run("w", { script: "s", args: { op: "inventory" } })
319 ]);
320 assert.equal(peak, 1, "never two helpers at once on one session");
321 assert.deepEqual(order, ["list", "boom", "metrics", "inventory"]);
322 assert.deepEqual(results.map((result) => result.status), ["fulfilled", "rejected", "fulfilled", "fulfilled"]);
323 assert.equal(results[3].value.op, "inventory", "the queue continues after a failed helper");
324});
SHA-256f5a6506e918fafcdb108c39bc0f922f77dddb9d56ec02c16bb764870419d9786

CH-J Proprietary Software License 1.14 · Kilden leveres under CH-J Proprietary Software License 1.14. Dens tilgængelighed ændrer ikke licensbetingelserne eller giver yderligere rettigheder.

Arkiv SHA-256476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Filer