@orb44/cli 0.1.1 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/bin/orb44.mjs +236 -25
- package/package.json +4 -2
- package/server/pulse.mjs +98 -37
- package/server/sat-i18n.mjs +164 -8
- package/server/sat-local.mjs +46 -0
- package/server/sat-update.mjs +104 -0
package/README.md
CHANGED
|
@@ -13,10 +13,13 @@ npx @orb44/cli login --url https://orb44.com
|
|
|
13
13
|
Open the printed link in a browser where you are already signed in, confirm the domain, then optionally install the systemd daemon (default no). After a global install the command is `orb44`.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @orb44/cli status
|
|
17
|
-
npx @orb44/cli pulse
|
|
18
|
-
npx @orb44/cli install # later: background pulse, starts after reboot
|
|
19
|
-
|
|
16
|
+
npx @orb44/cli@0.1.5 status
|
|
17
|
+
npx @orb44/cli@0.1.5 pulse
|
|
18
|
+
npx @orb44/cli@0.1.5 install # later: background pulse, starts after reboot
|
|
19
|
+
orb44 update # after install: replace /usr/lib/orb44-sat from npm
|
|
20
|
+
npx @orb44/cli@0.1.5 logout
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
Do not run bare `npx @orb44/cli` — it can reuse an old cache. Pin the version or use `orb44` after install.
|
|
24
|
+
|
|
22
25
|
Device key: `~/.config/orb44/device.json` (mode 0600). Not a shell token.
|
package/bin/orb44.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
8
8
|
import { collectPulse, formatPulsePreview } from "../server/pulse.mjs";
|
|
9
9
|
import { LANGS, LANG_LABEL, detectLang, normalizeLang, t } from "../server/sat-i18n.mjs";
|
|
10
10
|
import { pickFromList } from "../server/cli-menu.mjs";
|
|
11
|
+
import { SYSTEM_DEVICE, parseDeviceJson, hasExistingInstall, pickLiveDevice, deviceSearchPaths, loadFirstDevice, stripDevicePath } from "../server/sat-local.mjs";
|
|
12
|
+
import { CLI_PACKAGE, cmpVer, readCliVersion, pickSatRoots, applyUpdateTree, fetchLatestMeta, unpackTarball } from "../server/sat-update.mjs";
|
|
11
13
|
|
|
12
14
|
const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
|
|
13
15
|
const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
|
|
@@ -21,6 +23,8 @@ function args() {
|
|
|
21
23
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
22
24
|
else if (a === "--system") out.system = true;
|
|
23
25
|
else if (a === "--daemon") out.daemon = true;
|
|
26
|
+
else if (a === "--force") out.force = true;
|
|
27
|
+
else if (a === "--purge") out.purge = true;
|
|
24
28
|
else if (a === "--url" || a === "--code" || a === "--name" || a === "--interval" || a === "--lang") {
|
|
25
29
|
out[a.slice(2)] = argv[++i];
|
|
26
30
|
} else if (!a.startsWith("-")) out._.push(a);
|
|
@@ -57,12 +61,12 @@ function storedOrFlag(opts) {
|
|
|
57
61
|
return normalizeLang(opts.lang) || storedLang() || detectLang();
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function loadDeviceRecord() {
|
|
65
|
+
return loadFirstDevice((p) => fs.readFileSync(p, "utf8"), deviceSearchPaths(DEVICE_FILE));
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function loadDevice() {
|
|
61
|
-
|
|
62
|
-
return JSON.parse(fs.readFileSync(DEVICE_FILE, "utf8"));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
69
|
+
return stripDevicePath(loadDeviceRecord());
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function saveDevice(row) {
|
|
@@ -180,14 +184,90 @@ async function sendPulse(device, { preview = true } = {}) {
|
|
|
180
184
|
return { out, pulse };
|
|
181
185
|
}
|
|
182
186
|
|
|
187
|
+
async function wantReplace(found, opts) {
|
|
188
|
+
if (opts.force) return true;
|
|
189
|
+
if (opts.yes || !input.isTTY || !output.isTTY) return false;
|
|
190
|
+
const lines = [t(lang, "already_title")];
|
|
191
|
+
for (const d of found.devices) {
|
|
192
|
+
lines.push(t(lang, "already_host", { host: d.host || "—", name: d.name || "—" }));
|
|
193
|
+
lines.push(t(lang, "already_key", { file: d.path }));
|
|
194
|
+
}
|
|
195
|
+
if (found.daemon) lines.push(t(lang, "already_daemon"));
|
|
196
|
+
else if (found.unit) lines.push(t(lang, "already_unit"));
|
|
197
|
+
const idx = await pickFromList({
|
|
198
|
+
title: lines.join("\n"),
|
|
199
|
+
items: [t(lang, "already_keep"), t(lang, "already_replace")],
|
|
200
|
+
index: 0,
|
|
201
|
+
hint: "↑↓ Enter",
|
|
202
|
+
stdin: input,
|
|
203
|
+
stdout: output,
|
|
204
|
+
});
|
|
205
|
+
return idx === 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function existingOnBox() {
|
|
209
|
+
const paths = [...new Set([DEVICE_FILE, SYSTEM_DEVICE])];
|
|
210
|
+
const devices = [];
|
|
211
|
+
for (const p of paths) {
|
|
212
|
+
try {
|
|
213
|
+
const row = parseDeviceJson(fs.readFileSync(p, "utf8"), p);
|
|
214
|
+
if (row) devices.push(row);
|
|
215
|
+
} catch {
|
|
216
|
+
/* missing or unreadable */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const unit =
|
|
220
|
+
fs.existsSync("/etc/systemd/system/orb44-satellite.service") ||
|
|
221
|
+
fs.existsSync(path.join(os.homedir(), ".config", "systemd", "user", "orb44-satellite.service"));
|
|
222
|
+
let daemon = false;
|
|
223
|
+
try {
|
|
224
|
+
daemon = serviceIsActive(true) || serviceIsActive(false);
|
|
225
|
+
} catch {
|
|
226
|
+
daemon = false;
|
|
227
|
+
}
|
|
228
|
+
return { devices, daemon, unit };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function revokeLocalDevices(devices, fallbackApi) {
|
|
232
|
+
for (const d of devices) {
|
|
233
|
+
const apiBase = d.api || fallbackApi;
|
|
234
|
+
if (!apiBase || !d.secret) continue;
|
|
235
|
+
await api(apiBase, "/api/satellites/logout", { method: "POST", json: { secret: d.secret } }).catch(() => {});
|
|
236
|
+
}
|
|
237
|
+
clearDevice();
|
|
238
|
+
if (SYSTEM_DEVICE !== DEVICE_FILE) {
|
|
239
|
+
try {
|
|
240
|
+
fs.unlinkSync(SYSTEM_DEVICE);
|
|
241
|
+
} catch {
|
|
242
|
+
/* missing */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
183
247
|
async function cmdLogin(opts) {
|
|
184
248
|
await ensureLang(opts);
|
|
185
249
|
banner();
|
|
186
250
|
const url = String(opts.url || process.env.ORB44_API || "http://127.0.0.1:8787").replace(/\/$/, "");
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
const found = existingOnBox();
|
|
252
|
+
if (hasExistingInstall(found)) {
|
|
253
|
+
const replace = await wantReplace(found, opts);
|
|
254
|
+
if (!replace) {
|
|
255
|
+
const live = pickLiveDevice(found.devices, { daemon: found.daemon });
|
|
256
|
+
if (!live?.secret) {
|
|
257
|
+
console.log(t(lang, "already_no_key"));
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
const { out } = await sendPulse(live);
|
|
261
|
+
if (!out.ok) {
|
|
262
|
+
console.error(`⚠️ ${out.body.error || t(lang, "pulse_fail")}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
266
|
+
printAdvice(out.body?.notes);
|
|
267
|
+
console.log(dim("\n" + t(lang, "already_kept")));
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
await revokeLocalDevices(found.devices, url);
|
|
191
271
|
}
|
|
192
272
|
const hostname = os.hostname();
|
|
193
273
|
const name = opts.name || hostname;
|
|
@@ -336,24 +416,26 @@ function systemdUnit({ node, script, deviceFile, interval, user }) {
|
|
|
336
416
|
return `${lines.join("\n")}\n`;
|
|
337
417
|
}
|
|
338
418
|
|
|
339
|
-
|
|
340
|
-
|
|
419
|
+
const CLI_LINK = "/usr/local/bin/orb44";
|
|
420
|
+
|
|
421
|
+
function linkSystemCli(node, script) {
|
|
422
|
+
const body = `#!/bin/sh\nexec ${quote(node)} ${quote(script)} "$@"\n`;
|
|
423
|
+
fs.writeFileSync(CLI_LINK, body, { mode: 0o755 });
|
|
341
424
|
}
|
|
342
425
|
|
|
343
426
|
function installSystemTree() {
|
|
344
427
|
const lib = "/usr/lib/orb44-sat";
|
|
345
|
-
const
|
|
428
|
+
const srcRoot = path.join(path.dirname(SCRIPT), "..");
|
|
346
429
|
fs.mkdirSync(path.join(lib, "bin"), { recursive: true, mode: 0o755 });
|
|
347
430
|
fs.mkdirSync(path.join(lib, "server"), { recursive: true, mode: 0o755 });
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
fs.chmodSync(to, 0o644);
|
|
431
|
+
applyUpdateTree(srcRoot, lib);
|
|
432
|
+
const ver = readCliVersion(SCRIPT);
|
|
433
|
+
if (ver && ver !== "0.0.0") {
|
|
434
|
+
fs.writeFileSync(
|
|
435
|
+
path.join(lib, "package.json"),
|
|
436
|
+
JSON.stringify({ name: CLI_PACKAGE, version: ver, type: "module", private: true }, null, 2) + "\n",
|
|
437
|
+
{ mode: 0o644 }
|
|
438
|
+
);
|
|
357
439
|
}
|
|
358
440
|
return path.join(lib, "bin", "orb44.mjs");
|
|
359
441
|
}
|
|
@@ -377,7 +459,9 @@ function ensureSystemServiceAccount(srcDeviceFile) {
|
|
|
377
459
|
}
|
|
378
460
|
}
|
|
379
461
|
fs.mkdirSync(home, { recursive: true, mode: 0o750 });
|
|
380
|
-
|
|
462
|
+
if (path.resolve(srcDeviceFile) !== path.resolve(dest)) {
|
|
463
|
+
fs.copyFileSync(srcDeviceFile, dest);
|
|
464
|
+
}
|
|
381
465
|
fs.chmodSync(dest, 0o600);
|
|
382
466
|
const chown = spawnSync("chown", ["-R", "orb44:orb44", home], { encoding: "utf8" });
|
|
383
467
|
if (chown.status !== 0) {
|
|
@@ -429,18 +513,22 @@ function tryEnable(asSystem) {
|
|
|
429
513
|
|
|
430
514
|
function cmdInstall(opts, { enable = false } = {}) {
|
|
431
515
|
applyLang(opts);
|
|
432
|
-
const
|
|
433
|
-
if (!
|
|
516
|
+
const rec = loadDeviceRecord();
|
|
517
|
+
if (!rec?.secret) {
|
|
434
518
|
console.error("⚠️ " + t(lang, "need_login"));
|
|
435
519
|
process.exit(1);
|
|
436
520
|
}
|
|
521
|
+
const srcPath = rec.path || DEVICE_FILE;
|
|
522
|
+
const device = stripDevicePath(rec);
|
|
523
|
+
if (opts.url) device.api = String(opts.url).replace(/\/$/, "");
|
|
524
|
+
if (srcPath !== DEVICE_FILE) saveDevice(device);
|
|
437
525
|
const asSystem = Boolean(opts.system) || process.getuid?.() === 0;
|
|
438
526
|
const interval = intervalSec(opts.interval);
|
|
439
527
|
let deviceFile = DEVICE_FILE;
|
|
440
528
|
let user = null;
|
|
441
529
|
let script = SCRIPT;
|
|
442
530
|
if (asSystem) {
|
|
443
|
-
const acct = ensureSystemServiceAccount(
|
|
531
|
+
const acct = ensureSystemServiceAccount(srcPath);
|
|
444
532
|
if (acct.ok) {
|
|
445
533
|
deviceFile = acct.deviceFile;
|
|
446
534
|
user = acct.user;
|
|
@@ -463,6 +551,14 @@ function cmdInstall(opts, { enable = false } = {}) {
|
|
|
463
551
|
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
464
552
|
fs.writeFileSync(unitPath, unit, { mode: 0o644 });
|
|
465
553
|
console.log("⚙️ " + t(lang, "unit_written", { path: unitPath }));
|
|
554
|
+
if (asSystem) {
|
|
555
|
+
try {
|
|
556
|
+
linkSystemCli(process.execPath, script);
|
|
557
|
+
console.log(dim(t(lang, "cli_link", { path: CLI_LINK })));
|
|
558
|
+
} catch {
|
|
559
|
+
/* /usr/local/bin missing */
|
|
560
|
+
}
|
|
561
|
+
}
|
|
466
562
|
if (enable) {
|
|
467
563
|
const on = tryEnable(asSystem);
|
|
468
564
|
if (on.ok) {
|
|
@@ -482,6 +578,119 @@ function cmdInstall(opts, { enable = false } = {}) {
|
|
|
482
578
|
return false;
|
|
483
579
|
}
|
|
484
580
|
|
|
581
|
+
function tryDisable(asSystem) {
|
|
582
|
+
const args = asSystem
|
|
583
|
+
? [
|
|
584
|
+
["disable", "--now", "orb44-satellite"],
|
|
585
|
+
["daemon-reload"],
|
|
586
|
+
]
|
|
587
|
+
: [
|
|
588
|
+
["--user", "disable", "--now", "orb44-satellite"],
|
|
589
|
+
["--user", "daemon-reload"],
|
|
590
|
+
];
|
|
591
|
+
for (const a of args) {
|
|
592
|
+
spawnSync("systemctl", a, { encoding: "utf8" });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function rmTree(p) {
|
|
597
|
+
try {
|
|
598
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
599
|
+
} catch {
|
|
600
|
+
/* missing */
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function cmdUninstall(opts) {
|
|
605
|
+
applyLang(opts);
|
|
606
|
+
const asSystem = Boolean(opts.system) || process.getuid?.() === 0;
|
|
607
|
+
tryDisable(asSystem);
|
|
608
|
+
tryDisable(!asSystem);
|
|
609
|
+
const unitPaths = [
|
|
610
|
+
"/etc/systemd/system/orb44-satellite.service",
|
|
611
|
+
path.join(os.homedir(), ".config", "systemd", "user", "orb44-satellite.service"),
|
|
612
|
+
];
|
|
613
|
+
for (const p of unitPaths) {
|
|
614
|
+
try {
|
|
615
|
+
fs.unlinkSync(p);
|
|
616
|
+
} catch {
|
|
617
|
+
/* missing */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
rmTree("/usr/lib/orb44-sat");
|
|
621
|
+
try {
|
|
622
|
+
fs.unlinkSync(CLI_LINK);
|
|
623
|
+
} catch {
|
|
624
|
+
/* missing */
|
|
625
|
+
}
|
|
626
|
+
if (opts.purge) {
|
|
627
|
+
const found = existingOnBox();
|
|
628
|
+
await revokeLocalDevices(found.devices, found.devices[0]?.api);
|
|
629
|
+
rmTree("/var/lib/orb44");
|
|
630
|
+
spawnSync("userdel", ["orb44"], { encoding: "utf8" });
|
|
631
|
+
console.log("✅ " + t(lang, "uninstall_purged"));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
console.log("✅ " + t(lang, "uninstall_ok"));
|
|
635
|
+
console.log(dim(t(lang, "uninstall_key_kept", { file: SYSTEM_DEVICE })));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function cmdUpdate() {
|
|
639
|
+
applyLang(opts);
|
|
640
|
+
const current = readCliVersion(SCRIPT);
|
|
641
|
+
let meta;
|
|
642
|
+
try {
|
|
643
|
+
meta = await fetchLatestMeta();
|
|
644
|
+
} catch (e) {
|
|
645
|
+
console.error("⚠️ " + t(lang, "update_fail", { err: `: ${String(e.message || e).slice(0, 160)}` }));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const pin = t(lang, "update_npx", { version: meta.version });
|
|
649
|
+
if (cmpVer(current, meta.version) >= 0 && !opts.force) {
|
|
650
|
+
console.log("✅ " + t(lang, "update_same", { version: current }));
|
|
651
|
+
console.log(dim(pin));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const roots = pickSatRoots(SCRIPT);
|
|
655
|
+
if (!roots.length) {
|
|
656
|
+
console.error("⚠️ " + t(lang, "update_no_tree"));
|
|
657
|
+
console.log(dim(pin));
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "orb44-upd-"));
|
|
661
|
+
let wrote = [];
|
|
662
|
+
try {
|
|
663
|
+
const pkg = await unpackTarball(meta.tarball, tmp);
|
|
664
|
+
const blocked = [];
|
|
665
|
+
for (const root of roots) {
|
|
666
|
+
try {
|
|
667
|
+
applyUpdateTree(pkg, root);
|
|
668
|
+
wrote.push(root);
|
|
669
|
+
} catch (e) {
|
|
670
|
+
blocked.push({ root, err: String(e.message || e).slice(0, 160) });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!wrote.length) {
|
|
674
|
+
console.error("⚠️ " + t(lang, blocked.some((b) => /EACCES|permission/i.test(b.err)) ? "update_need_root" : "update_fail", { err: blocked[0]?.err ? `: ${blocked[0].err}` : "" }));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
} finally {
|
|
678
|
+
try {
|
|
679
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
680
|
+
} catch {
|
|
681
|
+
/* tmp */
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const root of wrote) {
|
|
685
|
+
console.log("✅ " + t(lang, "update_wrote", { version: meta.version, path: root }));
|
|
686
|
+
}
|
|
687
|
+
if (wrote.includes("/usr/lib/orb44-sat") && serviceIsActive(true)) {
|
|
688
|
+
spawnSync("systemctl", ["try-restart", "orb44-satellite"], { encoding: "utf8" });
|
|
689
|
+
console.log(dim(t(lang, "update_restarted")));
|
|
690
|
+
}
|
|
691
|
+
console.log(dim(pin));
|
|
692
|
+
}
|
|
693
|
+
|
|
485
694
|
function cmdStatus() {
|
|
486
695
|
applyLang(opts);
|
|
487
696
|
const device = loadDevice();
|
|
@@ -548,7 +757,9 @@ const run = {
|
|
|
548
757
|
pulse: cmdPulse,
|
|
549
758
|
daemon: () => cmdDaemon(opts),
|
|
550
759
|
install: () => cmdInstall(opts, { enable: true }),
|
|
760
|
+
uninstall: () => cmdUninstall(opts),
|
|
551
761
|
status: cmdStatus,
|
|
762
|
+
update: cmdUpdate,
|
|
552
763
|
logout: cmdLogout,
|
|
553
764
|
lang: () => cmdLang(opts),
|
|
554
765
|
help,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orb44/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Orb44: street snapshot of your site. This CLI is the Watch satellite — pulse from the host (load, listeners, hardening). The cabinet does not run commands on the machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"bin/orb44.mjs",
|
|
11
11
|
"server/pulse.mjs",
|
|
12
12
|
"server/sat-i18n.mjs",
|
|
13
|
-
"server/cli-menu.mjs"
|
|
13
|
+
"server/cli-menu.mjs",
|
|
14
|
+
"server/sat-local.mjs",
|
|
15
|
+
"server/sat-update.mjs"
|
|
14
16
|
],
|
|
15
17
|
"engines": {
|
|
16
18
|
"node": ">=18"
|
package/server/pulse.mjs
CHANGED
|
@@ -174,11 +174,32 @@ function topTable() {
|
|
|
174
174
|
|
|
175
175
|
function okListenAddr(a) {
|
|
176
176
|
const s = String(a || "");
|
|
177
|
-
if (s === "0.0.0.0" || s === "::" || s === "::1") return true;
|
|
177
|
+
if (s === "0.0.0.0" || s === "::" || s === "::1" || s === "*") return true;
|
|
178
178
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(s)) return true;
|
|
179
179
|
return /^[0-9a-fA-F:]+$/.test(s) && s.includes(":");
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export function collapseListen(rows = []) {
|
|
183
|
+
const groups = new Map();
|
|
184
|
+
for (const r of rows) {
|
|
185
|
+
const key = `${Number(r.port)}|${r.comm || ""}`;
|
|
186
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
187
|
+
groups.get(key).push(r);
|
|
188
|
+
}
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const list of groups.values()) {
|
|
191
|
+
const addrs = new Set(list.map((x) => x.addr));
|
|
192
|
+
const rest = list.filter((x) => x.addr !== "0.0.0.0" && x.addr !== "::" && x.addr !== "*");
|
|
193
|
+
if (addrs.has("*") || (addrs.has("0.0.0.0") && addrs.has("::"))) {
|
|
194
|
+
out.push({ addr: "*", port: list[0].port, comm: list[0].comm });
|
|
195
|
+
out.push(...rest);
|
|
196
|
+
} else {
|
|
197
|
+
out.push(...list);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
182
203
|
export function sanitizePulse(raw = {}) {
|
|
183
204
|
const top = Array.isArray(raw.top)
|
|
184
205
|
? raw.top
|
|
@@ -190,16 +211,18 @@ export function sanitizePulse(raw = {}) {
|
|
|
190
211
|
}))
|
|
191
212
|
.filter((r) => r.comm)
|
|
192
213
|
: [];
|
|
193
|
-
const listen =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
const listen = collapseListen(
|
|
215
|
+
Array.isArray(raw.listen)
|
|
216
|
+
? raw.listen
|
|
217
|
+
.slice(0, 40)
|
|
218
|
+
.map((r) => ({
|
|
219
|
+
addr: String(r.addr || "").slice(0, 45),
|
|
220
|
+
port: Number(r.port) || 0,
|
|
221
|
+
comm: sanitizeComm(r.comm),
|
|
222
|
+
}))
|
|
223
|
+
.filter((r) => r.port > 0 && r.port < 65536 && okListenAddr(r.addr))
|
|
224
|
+
: []
|
|
225
|
+
);
|
|
203
226
|
const originA = Array.isArray(raw.originA)
|
|
204
227
|
? raw.originA.map((x) => String(x || "")).filter((x) => /^\d{1,3}(\.\d{1,3}){3}$/.test(x)).slice(0, 8)
|
|
205
228
|
: [];
|
|
@@ -672,33 +695,71 @@ export function collectPulse() {
|
|
|
672
695
|
});
|
|
673
696
|
}
|
|
674
697
|
|
|
698
|
+
function clip(s, n) {
|
|
699
|
+
const t = String(s ?? "");
|
|
700
|
+
if (t.length <= n) return t;
|
|
701
|
+
return `${t.slice(0, Math.max(1, n - 1))}…`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function asciiTable(headers, rows) {
|
|
705
|
+
const cells = [headers, ...rows].map((r) => r.map((c) => String(c ?? "")));
|
|
706
|
+
const widths = headers.map((_, i) => Math.max(1, ...cells.map((r) => [...(r[i] || "")].length)));
|
|
707
|
+
const fill = (ch) => widths.map((w) => ch.repeat(w + 2)).join("┼");
|
|
708
|
+
const line = (row) =>
|
|
709
|
+
"│ " +
|
|
710
|
+
row
|
|
711
|
+
.map((c, i) => {
|
|
712
|
+
const s = clip(c, widths[i]);
|
|
713
|
+
return s + " ".repeat(Math.max(0, widths[i] - [...s].length));
|
|
714
|
+
})
|
|
715
|
+
.join(" │ ") +
|
|
716
|
+
" │";
|
|
717
|
+
return [`┌${fill("─").replaceAll("┼", "┬")}┐`, line(headers), `├${fill("─")}┤`, ...rows.map(line), `└${fill("─").replaceAll("┼", "┴")}┘`].join("\n");
|
|
718
|
+
}
|
|
719
|
+
|
|
675
720
|
export function formatPulsePreview(pulse, lang = "en") {
|
|
676
721
|
const gb = (n) => (Number(n) / 1024 / 1024 / 1024).toFixed(1);
|
|
677
722
|
const none = t(lang, "preview_none");
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
723
|
+
const h = pulse.hardening || {};
|
|
724
|
+
const grade = pulse.gradeInside || gradeInside(pulse) || "—";
|
|
725
|
+
const ssh = h.sshPassword ? t(lang, "preview_ssh_pass") : h.sshPassword === false ? t(lang, "preview_ssh_key") : "—";
|
|
726
|
+
const machine = asciiTable(
|
|
727
|
+
[t(lang, "preview_col_check"), t(lang, "preview_col_value")],
|
|
728
|
+
[
|
|
729
|
+
[t(lang, "preview_row_host"), pulse.hostname || "—"],
|
|
730
|
+
[t(lang, "preview_row_load"), Number(pulse.load1).toFixed(2)],
|
|
731
|
+
[t(lang, "preview_row_ram"), `${gb(pulse.memUsed)} / ${gb(pulse.memTotal)}`],
|
|
732
|
+
[t(lang, "preview_row_disk"), `${pulse.diskUsedPct ?? "—"}%`],
|
|
733
|
+
[t(lang, "preview_row_grade"), grade],
|
|
734
|
+
]
|
|
735
|
+
);
|
|
736
|
+
const listenRows = collapseListen([...(pulse.listen || [])])
|
|
737
|
+
.sort((a, b) => {
|
|
738
|
+
const rank = (addr) =>
|
|
739
|
+
addr === "0.0.0.0" || addr === "::" || addr === "*" ? 0 : String(addr).startsWith("127.") || addr === "::1" ? 2 : 1;
|
|
740
|
+
return rank(a.addr) - rank(b.addr) || Number(a.port) - Number(b.port);
|
|
741
|
+
})
|
|
742
|
+
.slice(0, 16)
|
|
743
|
+
.map((r) => [r.addr || "—", String(r.port ?? ""), r.comm || "—"]);
|
|
744
|
+
const listen = asciiTable(
|
|
745
|
+
[t(lang, "preview_col_bind"), t(lang, "preview_col_port"), t(lang, "preview_col_proc")],
|
|
746
|
+
listenRows.length ? listenRows : [[none, "", ""]]
|
|
747
|
+
);
|
|
748
|
+
const topRows = (pulse.top || []).slice(0, 6).map((r) => [r.comm || "—", `${r.cpuPct}%`, `${r.rssMb}M`]);
|
|
749
|
+
const top = asciiTable(
|
|
750
|
+
[t(lang, "preview_col_proc"), t(lang, "preview_col_cpu"), t(lang, "preview_col_rss")],
|
|
751
|
+
topRows.length ? topRows : [[none, "", ""]]
|
|
752
|
+
);
|
|
753
|
+
const guard = asciiTable(
|
|
754
|
+
[t(lang, "preview_col_check"), t(lang, "preview_col_value")],
|
|
755
|
+
[
|
|
756
|
+
[t(lang, "preview_row_fw"), h.firewall || t(lang, "preview_fw_no")],
|
|
757
|
+
[t(lang, "preview_row_ban"), h.fail2ban || t(lang, "preview_ban_no")],
|
|
758
|
+
[t(lang, "preview_row_upd"), h.updates ? t(lang, "preview_yes") : t(lang, "preview_no")],
|
|
759
|
+
[t(lang, "preview_row_sync"), h.timesync || t(lang, "preview_no")],
|
|
760
|
+
[t(lang, "preview_row_ssh"), ssh],
|
|
761
|
+
]
|
|
762
|
+
);
|
|
763
|
+
const extra = pulse.limited ? `\n${t(lang, "preview_limited")}` : "";
|
|
764
|
+
return `${machine}\n\n${listen}\n\n${top}\n\n${guard}${extra}`;
|
|
704
765
|
}
|
package/server/sat-i18n.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function detectLang(env = process.env) {
|
|
|
21
21
|
const STR = {
|
|
22
22
|
en: {
|
|
23
23
|
banner_title: "Orb44 satellite",
|
|
24
|
-
banner_sub: "
|
|
24
|
+
banner_sub: "Inside analysis of this host: load, listeners, hardening.",
|
|
25
25
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
26
26
|
lang_saved: "Language saved: {label}",
|
|
27
27
|
lang_now: "CLI language: {label} (--lang en|ru|ko|es)",
|
|
@@ -45,6 +45,7 @@ const STR = {
|
|
|
45
45
|
daemon_run: "daemon every {sec}s · {host}",
|
|
46
46
|
daemon_revoked: "key revoked",
|
|
47
47
|
unit_written: "unit {path}",
|
|
48
|
+
cli_link: "command {path}",
|
|
48
49
|
daemon_on: "daemon enabled: background pulse, starts again after reboot.",
|
|
49
50
|
login_done: "Done. You can close this session — the daemon is already in the background.",
|
|
50
51
|
daemon_user: "service user {user} · key {file}",
|
|
@@ -55,6 +56,25 @@ const STR = {
|
|
|
55
56
|
not_paired: "Not paired. orb44 login",
|
|
56
57
|
now_on_box: "On the machine now",
|
|
57
58
|
logged_out: "Disconnected. Cabinet dropped the device.",
|
|
59
|
+
uninstall_ok: "Daemon stopped and unit removed. Pairing key kept.",
|
|
60
|
+
uninstall_purged: "Daemon, key, and service user orb44 removed. Cabinet dropped the device.",
|
|
61
|
+
uninstall_key_kept: "Key still at {file}. Later: npx @orb44/cli install",
|
|
62
|
+
already_title: "Already on this machine",
|
|
63
|
+
already_host: "{host} ({name})",
|
|
64
|
+
already_key: "key {file}",
|
|
65
|
+
already_daemon: "Watch daemon is already running",
|
|
66
|
+
already_unit: "systemd unit is already installed",
|
|
67
|
+
already_keep: "Keep — do not pair again",
|
|
68
|
+
already_replace: "Replace — new login, old device is revoked",
|
|
69
|
+
already_kept: "Left as is. Pulse sent. Same device in the cabinet.",
|
|
70
|
+
already_no_key: "Daemon is on, but this user cannot read the key. Try: sudo orb44 status",
|
|
71
|
+
update_same: "Already {version}.",
|
|
72
|
+
update_wrote: "Updated to {version} → {path}",
|
|
73
|
+
update_need_root: "Cannot write the install tree. Run: sudo orb44 update",
|
|
74
|
+
update_no_tree: "No satellite install to overwrite. After install: sudo orb44 update. Until then pin npx.",
|
|
75
|
+
update_fail: "Update failed{err}",
|
|
76
|
+
update_npx: "Do not run bare npx @orb44/cli — pin: npx @orb44/cli@{version}",
|
|
77
|
+
update_restarted: "Restarted orb44-satellite.",
|
|
58
78
|
advice_title: "What to do on this machine",
|
|
59
79
|
advice_sub: "The cabinet will not run this — a leaked key must not become a remote shell.",
|
|
60
80
|
preview_host: "host {h}",
|
|
@@ -62,6 +82,23 @@ const STR = {
|
|
|
62
82
|
preview_listen: "listening: {list}",
|
|
63
83
|
preview_top: "top: {list}",
|
|
64
84
|
preview_none: "not visible",
|
|
85
|
+
preview_col_check: "check",
|
|
86
|
+
preview_col_value: "value",
|
|
87
|
+
preview_col_bind: "bind",
|
|
88
|
+
preview_col_port: "port",
|
|
89
|
+
preview_col_proc: "process",
|
|
90
|
+
preview_col_cpu: "CPU",
|
|
91
|
+
preview_col_rss: "RSS",
|
|
92
|
+
preview_row_host: "host",
|
|
93
|
+
preview_row_load: "load",
|
|
94
|
+
preview_row_ram: "RAM",
|
|
95
|
+
preview_row_disk: "disk",
|
|
96
|
+
preview_row_grade: "grade",
|
|
97
|
+
preview_row_fw: "firewall",
|
|
98
|
+
preview_row_ban: "fail2ban",
|
|
99
|
+
preview_row_upd: "updates",
|
|
100
|
+
preview_row_sync: "time",
|
|
101
|
+
preview_row_ssh: "SSH",
|
|
65
102
|
preview_guard: "guard: firewall {fw} · {ban} · unattended-upgrades {upd} · time {sync} · SSH {ssh} · grade {grade}",
|
|
66
103
|
preview_fw_no: "none",
|
|
67
104
|
preview_ban_no: "no fail2ban",
|
|
@@ -72,12 +109,14 @@ const STR = {
|
|
|
72
109
|
preview_limited: "limited: cannot see which process owns the ports",
|
|
73
110
|
help: `Orb44 satellite — admin device.
|
|
74
111
|
|
|
75
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
112
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
76
113
|
orb44 pulse
|
|
77
114
|
orb44 daemon [--interval 300]
|
|
78
115
|
orb44 install [--system] [--interval 300]
|
|
116
|
+
orb44 uninstall [--purge]
|
|
79
117
|
orb44 lang [en|ru|ko|es]
|
|
80
118
|
orb44 status
|
|
119
|
+
orb44 update
|
|
81
120
|
orb44 logout
|
|
82
121
|
|
|
83
122
|
Login asks language (saved) then daemon (default no).
|
|
@@ -86,7 +125,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
86
125
|
},
|
|
87
126
|
ru: {
|
|
88
127
|
banner_title: "Orb44 сателлит",
|
|
89
|
-
banner_sub: "
|
|
128
|
+
banner_sub: "Анализ хоста изнутри: нагрузка, слушатели, hardening.",
|
|
90
129
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
91
130
|
lang_saved: "Язык сохранён: {label}",
|
|
92
131
|
lang_now: "Язык консоли: {label} (--lang en|ru|ko|es)",
|
|
@@ -110,6 +149,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
110
149
|
daemon_run: "демон каждые {sec}с · {host}",
|
|
111
150
|
daemon_revoked: "ключ отозван",
|
|
112
151
|
unit_written: "unit {path}",
|
|
152
|
+
cli_link: "command {path}",
|
|
113
153
|
daemon_on: "демон включён: пульс в фоне, поднимается после перезагрузки.",
|
|
114
154
|
login_done: "Готово. Эту сессию можно закрыть — демон уже в фоне.",
|
|
115
155
|
daemon_user: "пользователь службы {user} · ключ {file}",
|
|
@@ -120,6 +160,25 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
120
160
|
not_paired: "Не связано. orb44 login",
|
|
121
161
|
now_on_box: "Сейчас на машине",
|
|
122
162
|
logged_out: "Отключено. В кабинете устройство снято.",
|
|
163
|
+
uninstall_ok: "Демон остановлен, unit снят. Ключ пары оставлен.",
|
|
164
|
+
uninstall_purged: "Демон, ключ и пользователь orb44 сняты. В кабинете устройство отозвано.",
|
|
165
|
+
uninstall_key_kept: "Ключ на месте: {file}. Потом: npx @orb44/cli install",
|
|
166
|
+
already_title: "На этой машине уже стоит",
|
|
167
|
+
already_host: "{host} ({name})",
|
|
168
|
+
already_key: "ключ {file}",
|
|
169
|
+
already_daemon: "Демон Watch уже работает",
|
|
170
|
+
already_unit: "systemd unit уже установлен",
|
|
171
|
+
already_keep: "Оставить — не логиниться заново",
|
|
172
|
+
already_replace: "Заменить — новый login, старое устройство отзовётся",
|
|
173
|
+
already_kept: "Оставили как есть. Пульс ушёл. В кабинете то же устройство.",
|
|
174
|
+
already_no_key: "Демон работает, но этот пользователь не читает ключ. Попробуйте: sudo orb44 status",
|
|
175
|
+
update_same: "Уже {version}.",
|
|
176
|
+
update_wrote: "Обновлено до {version} → {path}",
|
|
177
|
+
update_need_root: "Нельзя записать дерево установки. Запустите: sudo orb44 update",
|
|
178
|
+
update_no_tree: "Нет дерева сателлита для перезаписи. После install: sudo orb44 update. До этого — pin в npx.",
|
|
179
|
+
update_fail: "Обновление не вышло{err}",
|
|
180
|
+
update_npx: "Не вызывайте голый npx @orb44/cli — pin: npx @orb44/cli@{version}",
|
|
181
|
+
update_restarted: "Демон orb44-satellite перезапущен.",
|
|
123
182
|
advice_title: "Что сделать на этой машине",
|
|
124
183
|
advice_sub: "Кабинет это не выполнит — иначе утечка ключа была бы удалённым шеллом.",
|
|
125
184
|
preview_host: "хост {h}",
|
|
@@ -127,6 +186,23 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
127
186
|
preview_listen: "слушает: {list}",
|
|
128
187
|
preview_top: "топ: {list}",
|
|
129
188
|
preview_none: "не видно",
|
|
189
|
+
preview_col_check: "проверка",
|
|
190
|
+
preview_col_value: "значение",
|
|
191
|
+
preview_col_bind: "адрес",
|
|
192
|
+
preview_col_port: "порт",
|
|
193
|
+
preview_col_proc: "процесс",
|
|
194
|
+
preview_col_cpu: "CPU",
|
|
195
|
+
preview_col_rss: "RSS",
|
|
196
|
+
preview_row_host: "хост",
|
|
197
|
+
preview_row_load: "load",
|
|
198
|
+
preview_row_ram: "RAM",
|
|
199
|
+
preview_row_disk: "диск",
|
|
200
|
+
preview_row_grade: "грейд",
|
|
201
|
+
preview_row_fw: "файрвол",
|
|
202
|
+
preview_row_ban: "fail2ban",
|
|
203
|
+
preview_row_upd: "обновления",
|
|
204
|
+
preview_row_sync: "время",
|
|
205
|
+
preview_row_ssh: "SSH",
|
|
130
206
|
preview_guard: "защита: файрвол {fw} · {ban} · автообновления {upd} · время {sync} · SSH {ssh} · грейд {grade}",
|
|
131
207
|
preview_fw_no: "нет",
|
|
132
208
|
preview_ban_no: "fail2ban нет",
|
|
@@ -137,12 +213,14 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
137
213
|
preview_limited: "ограничено: не видно, какой процесс слушает порты",
|
|
138
214
|
help: `Orb44 сателлит — устройство админа.
|
|
139
215
|
|
|
140
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
216
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
141
217
|
orb44 pulse
|
|
142
218
|
orb44 daemon [--interval 300]
|
|
143
219
|
orb44 install [--system] [--interval 300]
|
|
220
|
+
orb44 uninstall [--purge]
|
|
144
221
|
orb44 lang [en|ru|ko|es]
|
|
145
222
|
orb44 status
|
|
223
|
+
orb44 update
|
|
146
224
|
orb44 logout
|
|
147
225
|
|
|
148
226
|
После login спрашивает язык (запоминает), затем демон — по умолчанию нет.
|
|
@@ -151,7 +229,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
151
229
|
},
|
|
152
230
|
ko: {
|
|
153
231
|
banner_title: "Orb44 위성",
|
|
154
|
-
banner_sub: "
|
|
232
|
+
banner_sub: "이 호스트를 안에서 분석합니다: 부하, 리스너, hardening.",
|
|
155
233
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
156
234
|
lang_saved: "언어 저장됨: {label}",
|
|
157
235
|
lang_now: "CLI 언어: {label} (--lang en|ru|ko|es)",
|
|
@@ -175,6 +253,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
175
253
|
daemon_run: "데몬 {sec}초마다 · {host}",
|
|
176
254
|
daemon_revoked: "키 취소됨",
|
|
177
255
|
unit_written: "unit {path}",
|
|
256
|
+
cli_link: "command {path}",
|
|
178
257
|
daemon_on: "데몬이 켜졌습니다: 백그라운드 펄스, 재부팅 후에도 올라옵니다.",
|
|
179
258
|
login_done: "끝났습니다. 이 세션을 닫아도 됩니다 — 데몬은 이미 백그라운드에 있습니다.",
|
|
180
259
|
daemon_user: "서비스 사용자 {user} · 키 {file}",
|
|
@@ -185,6 +264,25 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
185
264
|
not_paired: "연결되지 않음. orb44 login",
|
|
186
265
|
now_on_box: "지금 이 기기",
|
|
187
266
|
logged_out: "해제됨. 콘솔에서 장치가 제거되었습니다.",
|
|
267
|
+
uninstall_ok: "데몬을 멈추고 unit을 제거했습니다. 페어링 키는 남겼습니다.",
|
|
268
|
+
uninstall_purged: "데몬, 키, 사용자 orb44를 제거했습니다. 콘솔에서 장치를 취소했습니다.",
|
|
269
|
+
uninstall_key_kept: "키는 그대로입니다: {file}. 나중에: npx @orb44/cli install",
|
|
270
|
+
already_title: "이 기기에 이미 있습니다",
|
|
271
|
+
already_host: "{host} ({name})",
|
|
272
|
+
already_key: "키 {file}",
|
|
273
|
+
already_daemon: "Watch 데몬이 이미 실행 중입니다",
|
|
274
|
+
already_unit: "systemd unit이 이미 설치되어 있습니다",
|
|
275
|
+
already_keep: "유지 — 다시 페어링하지 않음",
|
|
276
|
+
already_replace: "교체 — 새 login, 이전 장치는 취소됨",
|
|
277
|
+
already_kept: "그대로 두었습니다. 펄스를 보냈습니다. 콘솔의 장치는 같습니다.",
|
|
278
|
+
already_no_key: "데몬은 켜져 있지만 이 사용자는 키를 읽지 못합니다. sudo orb44 status",
|
|
279
|
+
update_same: "이미 {version}.",
|
|
280
|
+
update_wrote: "{version}(으)로 갱신 → {path}",
|
|
281
|
+
update_need_root: "설치 트리를 쓸 수 없습니다. sudo orb44 update",
|
|
282
|
+
update_no_tree: "덮어쓸 위성 설치가 없습니다. install 후 sudo orb44 update. 그 전에는 npx를 pin 하세요.",
|
|
283
|
+
update_fail: "업데이트 실패{err}",
|
|
284
|
+
update_npx: "버전 없는 npx @orb44/cli 는 쓰지 마세요 — pin: npx @orb44/cli@{version}",
|
|
285
|
+
update_restarted: "orb44-satellite 를 재시작했습니다.",
|
|
188
286
|
advice_title: "이 기기에서 할 일",
|
|
189
287
|
advice_sub: "콘솔은 실행하지 않습니다. 키 유출이 원격 셸이 되면 안 됩니다.",
|
|
190
288
|
preview_host: "호스트 {h}",
|
|
@@ -192,6 +290,23 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
192
290
|
preview_listen: "수신: {list}",
|
|
193
291
|
preview_top: "top: {list}",
|
|
194
292
|
preview_none: "없음",
|
|
293
|
+
preview_col_check: "항목",
|
|
294
|
+
preview_col_value: "값",
|
|
295
|
+
preview_col_bind: "주소",
|
|
296
|
+
preview_col_port: "포트",
|
|
297
|
+
preview_col_proc: "프로세스",
|
|
298
|
+
preview_col_cpu: "CPU",
|
|
299
|
+
preview_col_rss: "RSS",
|
|
300
|
+
preview_row_host: "호스트",
|
|
301
|
+
preview_row_load: "load",
|
|
302
|
+
preview_row_ram: "RAM",
|
|
303
|
+
preview_row_disk: "디스크",
|
|
304
|
+
preview_row_grade: "등급",
|
|
305
|
+
preview_row_fw: "방화벽",
|
|
306
|
+
preview_row_ban: "fail2ban",
|
|
307
|
+
preview_row_upd: "업데이트",
|
|
308
|
+
preview_row_sync: "시간",
|
|
309
|
+
preview_row_ssh: "SSH",
|
|
195
310
|
preview_guard: "보호: 방화벽 {fw} · {ban} · 자동업데이트 {upd} · 시간 {sync} · SSH {ssh} · 등급 {grade}",
|
|
196
311
|
preview_fw_no: "없음",
|
|
197
312
|
preview_ban_no: "fail2ban 없음",
|
|
@@ -202,12 +317,14 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
202
317
|
preview_limited: "제한: 포트를 연 프로세스를 볼 수 없음",
|
|
203
318
|
help: `Orb44 위성 — 관리자 장치.
|
|
204
319
|
|
|
205
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
320
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
206
321
|
orb44 pulse
|
|
207
322
|
orb44 daemon [--interval 300]
|
|
208
323
|
orb44 install [--system] [--interval 300]
|
|
324
|
+
orb44 uninstall [--purge]
|
|
209
325
|
orb44 lang [en|ru|ko|es]
|
|
210
326
|
orb44 status
|
|
327
|
+
orb44 update
|
|
211
328
|
orb44 logout
|
|
212
329
|
|
|
213
330
|
login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니다.
|
|
@@ -216,7 +333,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
216
333
|
},
|
|
217
334
|
es: {
|
|
218
335
|
banner_title: "Satélite Orb44",
|
|
219
|
-
banner_sub: "
|
|
336
|
+
banner_sub: "Análisis del host desde dentro: carga, listeners, hardening.",
|
|
220
337
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
221
338
|
lang_saved: "Idioma guardado: {label}",
|
|
222
339
|
lang_now: "Idioma de la consola: {label} (--lang en|ru|ko|es)",
|
|
@@ -240,6 +357,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
240
357
|
daemon_run: "demonio cada {sec}s · {host}",
|
|
241
358
|
daemon_revoked: "clave revocada",
|
|
242
359
|
unit_written: "unit {path}",
|
|
360
|
+
cli_link: "command {path}",
|
|
243
361
|
daemon_on: "demonio activado: pulso en segundo plano, arranca de nuevo tras el reinicio.",
|
|
244
362
|
login_done: "Listo. Puede cerrar esta sesión: el demonio ya está en segundo plano.",
|
|
245
363
|
daemon_user: "usuario del servicio {user} · clave {file}",
|
|
@@ -250,6 +368,25 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
250
368
|
not_paired: "Sin vincular. orb44 login",
|
|
251
369
|
now_on_box: "Ahora en la máquina",
|
|
252
370
|
logged_out: "Desconectado. El gabinete quitó el dispositivo.",
|
|
371
|
+
uninstall_ok: "Demonio parado y unit quitado. Se guarda la clave de emparejamiento.",
|
|
372
|
+
uninstall_purged: "Demonio, clave y usuario orb44 eliminados. El gabinete revocó el dispositivo.",
|
|
373
|
+
uninstall_key_kept: "La clave sigue en {file}. Luego: npx @orb44/cli install",
|
|
374
|
+
already_title: "Ya está en esta máquina",
|
|
375
|
+
already_host: "{host} ({name})",
|
|
376
|
+
already_key: "clave {file}",
|
|
377
|
+
already_daemon: "El demonio Watch ya está en marcha",
|
|
378
|
+
already_unit: "El unit systemd ya está instalado",
|
|
379
|
+
already_keep: "Dejar — no volver a emparejar",
|
|
380
|
+
already_replace: "Sustituir — login nuevo, se revoca el dispositivo anterior",
|
|
381
|
+
already_kept: "Se dejó igual. Pulso enviado. El mismo dispositivo en el gabinete.",
|
|
382
|
+
already_no_key: "El demonio está activo, pero este usuario no lee la clave. Pruebe: sudo orb44 status",
|
|
383
|
+
update_same: "Ya está {version}.",
|
|
384
|
+
update_wrote: "Actualizado a {version} → {path}",
|
|
385
|
+
update_need_root: "No se puede escribir el árbol. Ejecute: sudo orb44 update",
|
|
386
|
+
update_no_tree: "No hay instalación del satélite que sobrescribir. Tras install: sudo orb44 update. Mientras tanto, fije npx.",
|
|
387
|
+
update_fail: "La actualización falló{err}",
|
|
388
|
+
update_npx: "No use npx @orb44/cli sin versión — fije: npx @orb44/cli@{version}",
|
|
389
|
+
update_restarted: "orb44-satellite reiniciado.",
|
|
253
390
|
advice_title: "Qué hacer en esta máquina",
|
|
254
391
|
advice_sub: "El gabinete no lo ejecutará: una clave filtrada no debe ser un shell remoto.",
|
|
255
392
|
preview_host: "host {h}",
|
|
@@ -257,6 +394,23 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
257
394
|
preview_listen: "escucha: {list}",
|
|
258
395
|
preview_top: "top: {list}",
|
|
259
396
|
preview_none: "no se ve",
|
|
397
|
+
preview_col_check: "dato",
|
|
398
|
+
preview_col_value: "valor",
|
|
399
|
+
preview_col_bind: "bind",
|
|
400
|
+
preview_col_port: "puerto",
|
|
401
|
+
preview_col_proc: "proceso",
|
|
402
|
+
preview_col_cpu: "CPU",
|
|
403
|
+
preview_col_rss: "RSS",
|
|
404
|
+
preview_row_host: "host",
|
|
405
|
+
preview_row_load: "load",
|
|
406
|
+
preview_row_ram: "RAM",
|
|
407
|
+
preview_row_disk: "disco",
|
|
408
|
+
preview_row_grade: "grado",
|
|
409
|
+
preview_row_fw: "cortafuegos",
|
|
410
|
+
preview_row_ban: "fail2ban",
|
|
411
|
+
preview_row_upd: "updates",
|
|
412
|
+
preview_row_sync: "hora",
|
|
413
|
+
preview_row_ssh: "SSH",
|
|
260
414
|
preview_guard: "defensa: cortafuegos {fw} · {ban} · actualizaciones {upd} · hora {sync} · SSH {ssh} · grado {grade}",
|
|
261
415
|
preview_fw_no: "no",
|
|
262
416
|
preview_ban_no: "sin fail2ban",
|
|
@@ -267,12 +421,14 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
267
421
|
preview_limited: "limitado: no se ve qué proceso tiene los puertos",
|
|
268
422
|
help: `Satélite Orb44 — dispositivo de admin.
|
|
269
423
|
|
|
270
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
424
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
271
425
|
orb44 pulse
|
|
272
426
|
orb44 daemon [--interval 300]
|
|
273
427
|
orb44 install [--system] [--interval 300]
|
|
428
|
+
orb44 uninstall [--purge]
|
|
274
429
|
orb44 lang [en|ru|ko|es]
|
|
275
430
|
orb44 status
|
|
431
|
+
orb44 update
|
|
276
432
|
orb44 logout
|
|
277
433
|
|
|
278
434
|
Login pregunta idioma (lo guarda) y luego demonio (por defecto no).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const SYSTEM_DEVICE = "/var/lib/orb44/device.json";
|
|
2
|
+
|
|
3
|
+
export function parseDeviceJson(raw, filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const row = JSON.parse(raw);
|
|
6
|
+
if (!row?.secret) return null;
|
|
7
|
+
return { ...row, path: filePath };
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deviceSearchPaths(homeFile, systemdPath = SYSTEM_DEVICE) {
|
|
14
|
+
return [...new Set([homeFile, systemdPath].filter(Boolean))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadFirstDevice(readFile, paths) {
|
|
18
|
+
for (const p of paths) {
|
|
19
|
+
try {
|
|
20
|
+
const row = parseDeviceJson(readFile(p), p);
|
|
21
|
+
if (row) return row;
|
|
22
|
+
} catch {
|
|
23
|
+
/* missing or unreadable */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stripDevicePath(row) {
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
const { path: _p, ...rest } = row;
|
|
32
|
+
return rest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasExistingInstall({ devices = [], daemon = false, unit = false } = {}) {
|
|
36
|
+
return Boolean((devices && devices.length) || daemon || unit);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pickLiveDevice(devices, { daemon = false, systemdPath = SYSTEM_DEVICE } = {}) {
|
|
40
|
+
const list = Array.isArray(devices) ? devices.filter((d) => d?.secret) : [];
|
|
41
|
+
if (daemon) {
|
|
42
|
+
const sys = list.find((d) => d.path === systemdPath);
|
|
43
|
+
if (sys) return sys;
|
|
44
|
+
}
|
|
45
|
+
return list[0] || null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const CLI_PACKAGE = "@orb44/cli";
|
|
6
|
+
export const SAT_TREE_FILES = [
|
|
7
|
+
"bin/orb44.mjs",
|
|
8
|
+
"server/pulse.mjs",
|
|
9
|
+
"server/sat-i18n.mjs",
|
|
10
|
+
"server/cli-menu.mjs",
|
|
11
|
+
"server/sat-local.mjs",
|
|
12
|
+
"server/sat-update.mjs",
|
|
13
|
+
"package.json",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const REGISTRY_LATEST = "https://registry.npmjs.org/@orb44%2fcli/latest";
|
|
17
|
+
|
|
18
|
+
export function cmpVer(a, b) {
|
|
19
|
+
const pa = String(a || "0").split(".").map((n) => Number(n) || 0);
|
|
20
|
+
const pb = String(b || "0").split(".").map((n) => Number(n) || 0);
|
|
21
|
+
const n = Math.max(pa.length, pb.length);
|
|
22
|
+
for (let i = 0; i < n; i++) {
|
|
23
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
24
|
+
if (d) return d < 0 ? -1 : 1;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function looksLikeSat(root) {
|
|
30
|
+
const r = path.resolve(root);
|
|
31
|
+
if (!fs.existsSync(path.join(r, "bin", "orb44.mjs"))) return false;
|
|
32
|
+
if (!fs.existsSync(path.join(r, "server", "pulse.mjs"))) return false;
|
|
33
|
+
if (fs.existsSync(path.join(r, "server", "index.mjs"))) return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readCliVersion(scriptFile) {
|
|
38
|
+
const binDir = path.dirname(path.resolve(scriptFile));
|
|
39
|
+
const root = path.join(binDir, "..");
|
|
40
|
+
const candidates = [
|
|
41
|
+
path.join(root, "package.json"),
|
|
42
|
+
path.join(root, "packages", "orb44", "package.json"),
|
|
43
|
+
];
|
|
44
|
+
for (const p of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
47
|
+
if (j.name === CLI_PACKAGE && j.version) return String(j.version);
|
|
48
|
+
} catch {
|
|
49
|
+
/* next */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return "0.0.0";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function pickSatRoots(scriptFile, { systemLib = "/usr/lib/orb44-sat" } = {}) {
|
|
56
|
+
const here = path.resolve(path.dirname(scriptFile), "..");
|
|
57
|
+
const out = [];
|
|
58
|
+
if (looksLikeSat(here)) out.push(here);
|
|
59
|
+
const lib = systemLib ? path.resolve(systemLib) : "";
|
|
60
|
+
if (lib && looksLikeSat(lib) && !out.includes(lib)) out.push(lib);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function applyUpdateTree(srcPackageDir, destRoot) {
|
|
65
|
+
const src = path.resolve(srcPackageDir);
|
|
66
|
+
const dest = path.resolve(destRoot);
|
|
67
|
+
for (const rel of SAT_TREE_FILES) {
|
|
68
|
+
const from = path.join(src, rel);
|
|
69
|
+
if (!fs.existsSync(from)) {
|
|
70
|
+
if (rel === "package.json") continue;
|
|
71
|
+
throw new Error(`missing ${rel}`);
|
|
72
|
+
}
|
|
73
|
+
const to = path.join(dest, rel);
|
|
74
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
75
|
+
fs.copyFileSync(from, to);
|
|
76
|
+
fs.chmodSync(to, rel.endsWith("orb44.mjs") ? 0o755 : 0o644);
|
|
77
|
+
}
|
|
78
|
+
return dest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function fetchLatestMeta(fetchFn = fetch) {
|
|
82
|
+
const r = await fetchFn(REGISTRY_LATEST, { headers: { Accept: "application/json" } });
|
|
83
|
+
if (!r?.ok) throw new Error(`registry ${r?.status || "fail"}`);
|
|
84
|
+
const j = await r.json();
|
|
85
|
+
const version = j?.version;
|
|
86
|
+
const tarball = j?.dist?.tarball;
|
|
87
|
+
if (!version || !tarball) throw new Error("registry meta");
|
|
88
|
+
return { version: String(version), tarball: String(tarball) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function unpackTarball(url, tmp, fetchFn = fetch) {
|
|
92
|
+
const r = await fetchFn(url);
|
|
93
|
+
if (!r?.ok) throw new Error(`tarball ${r?.status || "fail"}`);
|
|
94
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
95
|
+
const tgz = path.join(tmp, "pkg.tgz");
|
|
96
|
+
fs.writeFileSync(tgz, buf);
|
|
97
|
+
const unpack = path.join(tmp, "unpack");
|
|
98
|
+
fs.mkdirSync(unpack, { recursive: true });
|
|
99
|
+
const tar = spawnSync("tar", ["-xzf", tgz, "-C", unpack], { encoding: "utf8" });
|
|
100
|
+
if (tar.status !== 0) throw new Error((tar.stderr || tar.stdout || "tar").trim().slice(0, 200));
|
|
101
|
+
const pkg = path.join(unpack, "package");
|
|
102
|
+
if (!fs.existsSync(path.join(pkg, "bin", "orb44.mjs"))) throw new Error("bad tarball");
|
|
103
|
+
return pkg;
|
|
104
|
+
}
|