@ebowwa/seedinstallation 0.3.0 → 0.4.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/LICENSE +1 -1
- package/README.md +36 -144
- package/dist/bootstrap.d.ts +5 -6
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +296 -215
- package/dist/clone.d.ts +1 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +54 -68
- package/dist/device-auth.d.ts +4 -5
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +254 -164
- package/dist/index.d.ts +13 -12
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +943 -10
- package/dist/runtime.d.ts +9 -14
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +213 -114
- package/dist/sudo.d.ts +1 -0
- package/dist/sudo.d.ts.map +1 -0
- package/dist/sudo.js +125 -222
- package/dist/systemd.d.ts +207 -3
- package/dist/systemd.d.ts.map +1 -0
- package/dist/systemd.js +479 -177
- package/package.json +40 -40
package/dist/systemd.js
CHANGED
|
@@ -1,186 +1,488 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
|
|
13
|
+
// sudo.ts
|
|
14
|
+
var exports_sudo = {};
|
|
15
|
+
__export(exports_sudo, {
|
|
16
|
+
writeFile: () => writeFile,
|
|
17
|
+
sudo: () => sudo,
|
|
18
|
+
serviceEnable: () => serviceEnable,
|
|
19
|
+
service: () => service,
|
|
20
|
+
pkgInstall: () => pkgInstall,
|
|
21
|
+
exec: () => exec
|
|
22
|
+
});
|
|
23
|
+
async function sudo(cmd, opts) {
|
|
24
|
+
const parts = Array.isArray(cmd) ? cmd : cmd.split(/\s+/);
|
|
25
|
+
const envPrefix = opts.env ? Object.entries(opts.env).map(([k, v]) => `${k}=${shellEscape(v)}`) : [];
|
|
26
|
+
const sudoCmd = ["sudo", ...envPrefix, ...parts];
|
|
27
|
+
return exec(sudoCmd, opts);
|
|
28
|
+
}
|
|
29
|
+
async function pkgInstall(packages, opts) {
|
|
30
|
+
const pm = opts.pm ?? "apt";
|
|
31
|
+
const envOverride = {
|
|
32
|
+
...opts.env,
|
|
33
|
+
...opts.nonInteractive !== false ? { DEBIAN_FRONTEND: "noninteractive" } : {}
|
|
34
|
+
};
|
|
35
|
+
await sudo(updateCmd[pm], { ...opts, env: envOverride, quiet: true });
|
|
36
|
+
return sudo([...installCmd[pm], ...packages], {
|
|
37
|
+
...opts,
|
|
38
|
+
env: envOverride
|
|
39
|
+
});
|
|
9
40
|
}
|
|
10
41
|
async function writeFile(path, content, opts) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
lines.push(`Group=${opts.group}`);
|
|
39
|
-
lines.push(`WorkingDirectory=${opts.workingDirectory}`);
|
|
40
|
-
lines.push(`ExecStart=${opts.execStart}`);
|
|
41
|
-
if (opts.execStartPre?.length) {
|
|
42
|
-
opts.execStartPre.forEach((cmd, i) => {
|
|
43
|
-
lines.push(`ExecStartPre${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (opts.execStopPost?.length) {
|
|
47
|
-
opts.execStopPost.forEach((cmd, i) => {
|
|
48
|
-
lines.push(`ExecStopPost${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
if (opts.restart)
|
|
52
|
-
lines.push(`Restart=${opts.restart}`);
|
|
53
|
-
if (opts.restartSec)
|
|
54
|
-
lines.push(`RestartSec=${opts.restartSec}`);
|
|
55
|
-
if (opts.environment) {
|
|
56
|
-
Object.entries(opts.environment).forEach(([k, v]) => {
|
|
57
|
-
lines.push(`Environment="${k}=${v}"`);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (opts.environmentFile) {
|
|
61
|
-
lines.push(`EnvironmentFile=${opts.environmentFile}`);
|
|
62
|
-
}
|
|
63
|
-
if (opts.standardOutput)
|
|
64
|
-
lines.push(`StandardOutput=${opts.standardOutput}`);
|
|
65
|
-
if (opts.standardError)
|
|
66
|
-
lines.push(`StandardError=${opts.standardError}`);
|
|
67
|
-
if (opts.nice !== undefined)
|
|
68
|
-
lines.push(`Nice=${opts.nice}`);
|
|
69
|
-
// Extras
|
|
70
|
-
if (opts.extras) {
|
|
71
|
-
lines.push("");
|
|
72
|
-
lines.push(opts.extras);
|
|
73
|
-
}
|
|
74
|
-
return lines.join("\n");
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Create and install a systemd service unit file.
|
|
78
|
-
*/
|
|
79
|
-
export async function createServiceUnit(name, opts) {
|
|
80
|
-
const unitPath = `/etc/systemd/system/${name}.service`;
|
|
81
|
-
const content = generateServiceUnit(name, opts);
|
|
82
|
-
// Write unit file
|
|
83
|
-
const writeResult = await writeFile(unitPath, content, {
|
|
84
|
-
context: opts.context,
|
|
85
|
-
mode: "644",
|
|
86
|
-
});
|
|
87
|
-
if (!writeResult.ok) {
|
|
88
|
-
return writeResult;
|
|
89
|
-
}
|
|
90
|
-
// Reload systemd
|
|
91
|
-
await exec(["systemctl", "daemon-reload"], opts);
|
|
92
|
-
return {
|
|
93
|
-
...writeResult,
|
|
94
|
-
unitPath,
|
|
95
|
-
};
|
|
42
|
+
const op = opts.append ? "-a" : "";
|
|
43
|
+
const teeCmd = `tee ${op} ${shellEscape(path)}`.trim();
|
|
44
|
+
const result = await execPipe(content, ["sudo", teeCmd], opts);
|
|
45
|
+
if (result.ok && opts.mode) {
|
|
46
|
+
await sudo(["chmod", opts.mode, path], opts);
|
|
47
|
+
}
|
|
48
|
+
if (result.ok && opts.owner) {
|
|
49
|
+
await sudo(["chown", opts.owner, path], opts);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
async function service(name, action, opts) {
|
|
54
|
+
return sudo(["systemctl", action, name], opts);
|
|
55
|
+
}
|
|
56
|
+
async function serviceEnable(name, opts) {
|
|
57
|
+
return sudo(["systemctl", "enable", "--now", name], opts);
|
|
58
|
+
}
|
|
59
|
+
function buildSshPrefix(ctx) {
|
|
60
|
+
const parts = ["ssh", "-F", "/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
|
|
61
|
+
if (ctx.keyPath)
|
|
62
|
+
parts.push("-i", ctx.keyPath);
|
|
63
|
+
else if (ctx.key)
|
|
64
|
+
parts.push("-i", ctx.key);
|
|
65
|
+
if (ctx.port)
|
|
66
|
+
parts.push("-p", String(ctx.port));
|
|
67
|
+
parts.push(`${ctx.user ?? "root"}@${ctx.host}`);
|
|
68
|
+
return parts;
|
|
96
69
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
export async function stopService(name, opts) {
|
|
122
|
-
return exec(["systemctl", "stop", name], opts);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Restart a service.
|
|
126
|
-
*/
|
|
127
|
-
export async function restartService(name, opts) {
|
|
128
|
-
return exec(["systemctl", "restart", name], opts);
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Reload a service (sends SIGHUP).
|
|
132
|
-
*/
|
|
133
|
-
export async function reloadService(name, opts) {
|
|
134
|
-
return exec(["systemctl", "reload", name], opts);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Get service status.
|
|
138
|
-
*/
|
|
139
|
-
export async function getServiceStatus(name, opts) {
|
|
140
|
-
const result = await exec(["systemctl", "show", name, "--property=LoadState,ActiveState,SubState,MainPID,Description"], {
|
|
141
|
-
...opts,
|
|
142
|
-
quiet: true,
|
|
70
|
+
async function exec(args, opts) {
|
|
71
|
+
const ctx = opts.context ?? { type: "local" };
|
|
72
|
+
const finalArgs = ctx.type === "ssh" ? [...buildSshPrefix(ctx), args.map(shellEscape).join(" ")] : args;
|
|
73
|
+
const proc = Bun.spawn(finalArgs, {
|
|
74
|
+
stdout: "pipe",
|
|
75
|
+
stderr: "pipe",
|
|
76
|
+
timeout: opts.timeout ?? 30000
|
|
77
|
+
});
|
|
78
|
+
const exitCode = await proc.exited;
|
|
79
|
+
const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
|
|
80
|
+
const stderr = await new Response(proc.stderr).text();
|
|
81
|
+
return { stdout, stderr, exitCode, ok: exitCode === 0 };
|
|
82
|
+
}
|
|
83
|
+
async function execPipe(input, args, opts) {
|
|
84
|
+
const ctx = opts.context ?? { type: "local" };
|
|
85
|
+
if (ctx.type === "ssh") {
|
|
86
|
+
const sshPrefix = buildSshPrefix(ctx);
|
|
87
|
+
const remoteCmd = args.join(" ");
|
|
88
|
+
const fullArgs = [...sshPrefix, remoteCmd];
|
|
89
|
+
const proc2 = Bun.spawn(fullArgs, {
|
|
90
|
+
stdin: new TextEncoder().encode(input),
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
timeout: opts.timeout ?? 30000
|
|
143
94
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
95
|
+
const exitCode2 = await proc2.exited;
|
|
96
|
+
const stdout2 = opts.quiet ? "" : await new Response(proc2.stdout).text();
|
|
97
|
+
const stderr2 = await new Response(proc2.stderr).text();
|
|
98
|
+
return { stdout: stdout2, stderr: stderr2, exitCode: exitCode2, ok: exitCode2 === 0 };
|
|
99
|
+
}
|
|
100
|
+
const proc = Bun.spawn(["sh", "-c", args.join(" ")], {
|
|
101
|
+
stdin: new TextEncoder().encode(input),
|
|
102
|
+
stdout: "pipe",
|
|
103
|
+
stderr: "pipe",
|
|
104
|
+
timeout: opts.timeout ?? 30000
|
|
105
|
+
});
|
|
106
|
+
const exitCode = await proc.exited;
|
|
107
|
+
const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
|
|
108
|
+
const stderr = await new Response(proc.stderr).text();
|
|
109
|
+
return { stdout, stderr, exitCode, ok: exitCode === 0 };
|
|
110
|
+
}
|
|
111
|
+
function shellEscape(s) {
|
|
112
|
+
if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(s))
|
|
113
|
+
return s;
|
|
114
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
115
|
+
}
|
|
116
|
+
var installCmd, updateCmd;
|
|
117
|
+
var init_sudo = __esm(() => {
|
|
118
|
+
installCmd = {
|
|
119
|
+
apt: ["apt-get", "install", "-y"],
|
|
120
|
+
dnf: ["dnf", "install", "-y"],
|
|
121
|
+
apk: ["apk", "add", "--no-cache"]
|
|
122
|
+
};
|
|
123
|
+
updateCmd = {
|
|
124
|
+
apt: ["apt-get", "update", "-qq"],
|
|
125
|
+
dnf: ["dnf", "check-update"],
|
|
126
|
+
apk: ["apk", "update"]
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// systemd.ts
|
|
131
|
+
async function exec2(args, opts) {
|
|
132
|
+
const { sudo: sudo2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
133
|
+
return sudo2(args, opts);
|
|
134
|
+
}
|
|
135
|
+
async function writeFile2(path, content, opts) {
|
|
136
|
+
const { writeFile: wf } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
137
|
+
return wf(path, content, opts);
|
|
138
|
+
}
|
|
139
|
+
var systemdCheckCache = null;
|
|
140
|
+
async function hasSystemd(opts) {
|
|
141
|
+
if (systemdCheckCache) {
|
|
142
|
+
return systemdCheckCache;
|
|
143
|
+
}
|
|
144
|
+
const sudoOpts = opts ?? { context: { type: "local" } };
|
|
145
|
+
const whichResult = await exec2(["which", "systemctl"], {
|
|
146
|
+
...sudoOpts,
|
|
147
|
+
quiet: true,
|
|
148
|
+
env: sudoOpts.env
|
|
149
|
+
});
|
|
150
|
+
if (!whichResult.ok || !whichResult.stdout.trim()) {
|
|
151
|
+
const result2 = {
|
|
152
|
+
available: false,
|
|
153
|
+
reason: "not_found",
|
|
154
|
+
message: "systemctl command not found - systemd is not installed or not in PATH"
|
|
155
|
+
};
|
|
156
|
+
systemdCheckCache = result2;
|
|
157
|
+
return result2;
|
|
158
|
+
}
|
|
159
|
+
const runningResult = await exec2(["systemctl", "is-system-running"], {
|
|
160
|
+
...sudoOpts,
|
|
161
|
+
quiet: true,
|
|
162
|
+
env: sudoOpts.env
|
|
163
|
+
});
|
|
164
|
+
if (runningResult.ok && runningResult.stdout.trim() !== "") {
|
|
165
|
+
const result2 = {
|
|
166
|
+
available: true,
|
|
167
|
+
message: "systemd is available and running"
|
|
150
168
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
systemdCheckCache = result2;
|
|
170
|
+
return result2;
|
|
171
|
+
}
|
|
172
|
+
const dirResult = await exec2(["test", "-d", "/run/systemd/system"], {
|
|
173
|
+
...sudoOpts,
|
|
174
|
+
quiet: true,
|
|
175
|
+
env: sudoOpts.env
|
|
176
|
+
});
|
|
177
|
+
if (dirResult.ok) {
|
|
178
|
+
const result2 = {
|
|
179
|
+
available: true,
|
|
180
|
+
message: "systemd detected via /run/systemd/system (container environment)"
|
|
157
181
|
};
|
|
182
|
+
systemdCheckCache = result2;
|
|
183
|
+
return result2;
|
|
184
|
+
}
|
|
185
|
+
const result = {
|
|
186
|
+
available: false,
|
|
187
|
+
reason: "not_running",
|
|
188
|
+
message: "systemd is not available on this system (container/initless environment)"
|
|
189
|
+
};
|
|
190
|
+
systemdCheckCache = result;
|
|
191
|
+
return result;
|
|
158
192
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
function clearSystemdCache() {
|
|
194
|
+
systemdCheckCache = null;
|
|
195
|
+
}
|
|
196
|
+
function generateServiceUnit(name, opts) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
lines.push("[Unit]");
|
|
199
|
+
lines.push(`Description=${opts.description}`);
|
|
200
|
+
if (opts.after?.length) {
|
|
201
|
+
lines.push(`After=${opts.after.join(" ")}`);
|
|
202
|
+
}
|
|
203
|
+
if (opts.wants?.length) {
|
|
204
|
+
lines.push(`Wants=${opts.wants.join(" ")}`);
|
|
205
|
+
}
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push("[Service]");
|
|
208
|
+
if (opts.type)
|
|
209
|
+
lines.push(`Type=${opts.type}`);
|
|
210
|
+
lines.push(`User=${opts.user ?? "root"}`);
|
|
211
|
+
if (opts.group)
|
|
212
|
+
lines.push(`Group=${opts.group}`);
|
|
213
|
+
lines.push(`WorkingDirectory=${opts.workingDirectory}`);
|
|
214
|
+
lines.push(`ExecStart=${opts.execStart}`);
|
|
215
|
+
if (opts.execStartPre?.length) {
|
|
216
|
+
opts.execStartPre.forEach((cmd, i) => {
|
|
217
|
+
lines.push(`ExecStartPre${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (opts.execStopPost?.length) {
|
|
221
|
+
opts.execStopPost.forEach((cmd, i) => {
|
|
222
|
+
lines.push(`ExecStopPost${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (opts.restart)
|
|
226
|
+
lines.push(`Restart=${opts.restart}`);
|
|
227
|
+
if (opts.restartSec)
|
|
228
|
+
lines.push(`RestartSec=${opts.restartSec}`);
|
|
229
|
+
if (opts.environment) {
|
|
230
|
+
Object.entries(opts.environment).forEach(([k, v]) => {
|
|
231
|
+
lines.push(`Environment="${k}=${v}"`);
|
|
172
232
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
233
|
+
}
|
|
234
|
+
if (opts.environmentFile) {
|
|
235
|
+
lines.push(`EnvironmentFile=${opts.environmentFile}`);
|
|
236
|
+
}
|
|
237
|
+
if (opts.standardOutput)
|
|
238
|
+
lines.push(`StandardOutput=${opts.standardOutput}`);
|
|
239
|
+
if (opts.standardError)
|
|
240
|
+
lines.push(`StandardError=${opts.standardError}`);
|
|
241
|
+
if (opts.nice !== undefined)
|
|
242
|
+
lines.push(`Nice=${opts.nice}`);
|
|
243
|
+
if (opts.noNewPrivileges)
|
|
244
|
+
lines.push("NoNewPrivileges=true");
|
|
245
|
+
if (opts.privateTmp)
|
|
246
|
+
lines.push("PrivateTmp=true");
|
|
247
|
+
if (opts.protectSystem)
|
|
248
|
+
lines.push(`ProtectSystem=${opts.protectSystem}`);
|
|
249
|
+
if (opts.protectHome === true)
|
|
250
|
+
lines.push("ProtectHome=true");
|
|
251
|
+
else if (opts.protectHome)
|
|
252
|
+
lines.push(`ProtectHome=${opts.protectHome}`);
|
|
253
|
+
if (opts.readOnlyPaths?.length)
|
|
254
|
+
lines.push(`ReadOnlyPaths=${opts.readOnlyPaths.join(" ")}`);
|
|
255
|
+
if (opts.readWritePaths?.length)
|
|
256
|
+
lines.push(`ReadWritePaths=${opts.readWritePaths.join(" ")}`);
|
|
257
|
+
if (opts.restrictRealtime)
|
|
258
|
+
lines.push("RestrictRealtime=true");
|
|
259
|
+
if (opts.memoryDenyWriteExecute)
|
|
260
|
+
lines.push("MemoryDenyWriteExecute=true");
|
|
261
|
+
if (opts.protectKernelTunables)
|
|
262
|
+
lines.push("ProtectKernelTunables=true");
|
|
263
|
+
if (opts.protectKernelModules)
|
|
264
|
+
lines.push("ProtectKernelModules=true");
|
|
265
|
+
if (opts.protectControlGroups)
|
|
266
|
+
lines.push("ProtectControlGroups=true");
|
|
267
|
+
if (opts.restrictAddressFamilies?.length)
|
|
268
|
+
lines.push(`RestrictAddressFamilies=${opts.restrictAddressFamilies.join(" ")}`);
|
|
269
|
+
if (opts.deviceAllow?.length)
|
|
270
|
+
lines.push(`DeviceAllow=${opts.deviceAllow.join(" ")}`);
|
|
271
|
+
if (opts.devicePolicy)
|
|
272
|
+
lines.push(`DevicePolicy=${opts.devicePolicy}`);
|
|
273
|
+
if (opts.systemCallFilter?.length)
|
|
274
|
+
lines.push(`SystemCallFilter=${opts.systemCallFilter.join(" ")}`);
|
|
275
|
+
if (opts.restrictNamespaces === true)
|
|
276
|
+
lines.push("RestrictNamespaces=true");
|
|
277
|
+
else if (Array.isArray(opts.restrictNamespaces) && opts.restrictNamespaces.length > 0)
|
|
278
|
+
lines.push(`RestrictNamespaces=${opts.restrictNamespaces.join(" ")}`);
|
|
279
|
+
if (opts.personality)
|
|
280
|
+
lines.push(`Personality=${opts.personality}`);
|
|
281
|
+
if (opts.lockPersonality)
|
|
282
|
+
lines.push("LockPersonality=true");
|
|
283
|
+
if (opts.removeCapability?.length)
|
|
284
|
+
opts.removeCapability.forEach((cap) => lines.push(`RemoveCapability=${cap}`));
|
|
285
|
+
if (opts.capabilityBoundingSet?.length)
|
|
286
|
+
lines.push(`CapabilityBoundingSet=${opts.capabilityBoundingSet.join(" ")}`);
|
|
287
|
+
if (opts.ambientCapabilities?.length)
|
|
288
|
+
lines.push(`AmbientCapabilities=${opts.ambientCapabilities.join(" ")}`);
|
|
289
|
+
if (opts.privateDevices)
|
|
290
|
+
lines.push("PrivateDevices=true");
|
|
291
|
+
if (opts.protectKernelLogs)
|
|
292
|
+
lines.push("ProtectKernelLogs=true");
|
|
293
|
+
if (opts.extras) {
|
|
294
|
+
lines.push("");
|
|
295
|
+
lines.push(opts.extras);
|
|
296
|
+
}
|
|
297
|
+
return lines.join(`
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
async function createServiceUnit(name, opts) {
|
|
301
|
+
const unitPath = `/etc/systemd/system/${name}.service`;
|
|
302
|
+
const content = generateServiceUnit(name, opts);
|
|
303
|
+
const writeResult = await writeFile2(unitPath, content, {
|
|
304
|
+
context: opts.context,
|
|
305
|
+
mode: "644",
|
|
306
|
+
env: opts.env,
|
|
307
|
+
timeout: opts.timeout
|
|
308
|
+
});
|
|
309
|
+
if (!writeResult.ok) {
|
|
310
|
+
return writeResult;
|
|
311
|
+
}
|
|
312
|
+
await exec2(["systemctl", "daemon-reload"], opts);
|
|
313
|
+
return {
|
|
314
|
+
...writeResult,
|
|
315
|
+
unitPath
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function enableService(name, opts) {
|
|
319
|
+
return exec2(["systemctl", "enable", name], opts);
|
|
320
|
+
}
|
|
321
|
+
async function disableService(name, opts) {
|
|
322
|
+
return exec2(["systemctl", "disable", name], opts);
|
|
323
|
+
}
|
|
324
|
+
async function startService(name, opts) {
|
|
325
|
+
return exec2(["systemctl", "start", name], opts);
|
|
326
|
+
}
|
|
327
|
+
async function stopService(name, opts) {
|
|
328
|
+
return exec2(["systemctl", "stop", name], opts);
|
|
329
|
+
}
|
|
330
|
+
async function restartService(name, opts) {
|
|
331
|
+
return exec2(["systemctl", "restart", name], opts);
|
|
332
|
+
}
|
|
333
|
+
async function reloadService(name, opts) {
|
|
334
|
+
return exec2(["systemctl", "reload", name], opts);
|
|
335
|
+
}
|
|
336
|
+
async function getServiceStatus(name, opts) {
|
|
337
|
+
const result = await exec2(["systemctl", "show", name, "--property=LoadState,ActiveState,SubState,MainPID,Description"], {
|
|
338
|
+
...opts,
|
|
339
|
+
quiet: true
|
|
340
|
+
});
|
|
341
|
+
if (!result.ok) {
|
|
342
|
+
return { loaded: false, active: false, subState: "unknown", mainPid: 0, description: "" };
|
|
343
|
+
}
|
|
344
|
+
const parse = (key) => {
|
|
345
|
+
const match = result.stdout.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
346
|
+
return match?.[1]?.trim() ?? "";
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
loaded: parse("LoadState") === "loaded",
|
|
350
|
+
active: parse("ActiveState") === "active",
|
|
351
|
+
subState: parse("SubState"),
|
|
352
|
+
mainPid: parseInt(parse("MainPID") || "0", 10),
|
|
353
|
+
description: parse("Description")
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function enableAndStartService(name, opts) {
|
|
357
|
+
return exec2(["systemctl", "enable", "--now", name], opts);
|
|
358
|
+
}
|
|
359
|
+
async function serviceExists(name, opts) {
|
|
360
|
+
const result = await exec2(["systemctl", "list-unit-files", name + ".service"], {
|
|
361
|
+
...opts,
|
|
362
|
+
quiet: true
|
|
363
|
+
});
|
|
364
|
+
return result.ok && result.stdout.includes(name);
|
|
365
|
+
}
|
|
366
|
+
async function getServiceLogs(name, opts = {}) {
|
|
367
|
+
const args = ["journalctl", "-u", name, "-n", String(opts.lines ?? 100)];
|
|
368
|
+
if (opts.since)
|
|
369
|
+
args.push("--since", opts.since);
|
|
370
|
+
if (!opts.follow)
|
|
371
|
+
args.push("-n", String(opts.lines ?? 100));
|
|
372
|
+
const result = await exec2(args, opts);
|
|
373
|
+
return result.stdout;
|
|
374
|
+
}
|
|
375
|
+
var SECURITY_PRESETS = {
|
|
376
|
+
strict: {
|
|
377
|
+
noNewPrivileges: true,
|
|
378
|
+
privateTmp: true,
|
|
379
|
+
protectSystem: "strict",
|
|
380
|
+
protectHome: true,
|
|
381
|
+
readOnlyPaths: ["/"],
|
|
382
|
+
readWritePaths: [],
|
|
383
|
+
restrictRealtime: true,
|
|
384
|
+
memoryDenyWriteExecute: true,
|
|
385
|
+
protectKernelTunables: true,
|
|
386
|
+
protectKernelModules: true,
|
|
387
|
+
protectControlGroups: true,
|
|
388
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
389
|
+
devicePolicy: "closed",
|
|
390
|
+
deviceAllow: [],
|
|
391
|
+
restrictNamespaces: true,
|
|
392
|
+
privateDevices: true,
|
|
393
|
+
protectKernelLogs: true,
|
|
394
|
+
lockPersonality: true,
|
|
395
|
+
capabilityBoundingSet: []
|
|
396
|
+
},
|
|
397
|
+
standard: {
|
|
398
|
+
noNewPrivileges: true,
|
|
399
|
+
privateTmp: true,
|
|
400
|
+
protectSystem: "full",
|
|
401
|
+
protectHome: true,
|
|
402
|
+
restrictRealtime: true,
|
|
403
|
+
memoryDenyWriteExecute: true,
|
|
404
|
+
protectKernelTunables: true,
|
|
405
|
+
protectKernelModules: true,
|
|
406
|
+
protectControlGroups: true,
|
|
407
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
408
|
+
devicePolicy: "auto",
|
|
409
|
+
restrictNamespaces: true,
|
|
410
|
+
privateDevices: true,
|
|
411
|
+
capabilityBoundingSet: ["CAP_NET_BIND_SERVICE"]
|
|
412
|
+
},
|
|
413
|
+
network: {
|
|
414
|
+
noNewPrivileges: true,
|
|
415
|
+
privateTmp: true,
|
|
416
|
+
protectSystem: "full",
|
|
417
|
+
protectHome: true,
|
|
418
|
+
restrictRealtime: true,
|
|
419
|
+
protectKernelTunables: true,
|
|
420
|
+
protectKernelModules: true,
|
|
421
|
+
protectControlGroups: true,
|
|
422
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
423
|
+
devicePolicy: "auto",
|
|
424
|
+
privateDevices: true,
|
|
425
|
+
capabilityBoundingSet: ["CAP_NET_BIND_SERVICE"],
|
|
426
|
+
ambientCapabilities: ["CAP_NET_BIND_SERVICE"]
|
|
427
|
+
},
|
|
428
|
+
minimal: {
|
|
429
|
+
noNewPrivileges: true,
|
|
430
|
+
privateTmp: true,
|
|
431
|
+
protectSystem: "yes",
|
|
432
|
+
restrictRealtime: true
|
|
433
|
+
},
|
|
434
|
+
deviceAccess: {
|
|
435
|
+
noNewPrivileges: true,
|
|
436
|
+
privateTmp: true,
|
|
437
|
+
protectSystem: "full",
|
|
438
|
+
protectHome: true,
|
|
439
|
+
restrictRealtime: true,
|
|
440
|
+
memoryDenyWriteExecute: true,
|
|
441
|
+
protectKernelTunables: true,
|
|
442
|
+
protectKernelModules: true,
|
|
443
|
+
protectControlGroups: true,
|
|
444
|
+
devicePolicy: "auto",
|
|
445
|
+
restrictNamespaces: true,
|
|
446
|
+
privateDevices: false
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
function withSecurityPreset(preset, customOptions) {
|
|
450
|
+
return {
|
|
451
|
+
...SECURITY_PRESETS[preset],
|
|
452
|
+
...customOptions
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function safeSystemd(operation, options = {}) {
|
|
456
|
+
const { requireSystemd = true, onNoSystemd, context, env, timeout, quiet } = options;
|
|
457
|
+
const sudoOpts = context ? { context, env, timeout, quiet } : { context: { type: "local" }, env, timeout, quiet };
|
|
458
|
+
const check = await hasSystemd(sudoOpts);
|
|
459
|
+
if (!check.available) {
|
|
460
|
+
if (onNoSystemd) {
|
|
461
|
+
await onNoSystemd(check);
|
|
462
|
+
}
|
|
463
|
+
if (requireSystemd) {
|
|
464
|
+
throw new Error(`Systemd required but not available: ${check.message}`);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
return operation();
|
|
186
469
|
}
|
|
470
|
+
export {
|
|
471
|
+
withSecurityPreset,
|
|
472
|
+
stopService,
|
|
473
|
+
startService,
|
|
474
|
+
serviceExists,
|
|
475
|
+
safeSystemd,
|
|
476
|
+
restartService,
|
|
477
|
+
reloadService,
|
|
478
|
+
hasSystemd,
|
|
479
|
+
getServiceStatus,
|
|
480
|
+
getServiceLogs,
|
|
481
|
+
generateServiceUnit,
|
|
482
|
+
enableService,
|
|
483
|
+
enableAndStartService,
|
|
484
|
+
disableService,
|
|
485
|
+
createServiceUnit,
|
|
486
|
+
clearSystemdCache,
|
|
487
|
+
SECURITY_PRESETS
|
|
488
|
+
};
|