unbound-cli 1.1.5 → 1.1.6
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/package.json +1 -1
- package/src/commands/onboard.js +8 -4
- package/src/commands/setup.js +79 -29
- package/src/output.js +1 -1
package/package.json
CHANGED
package/src/commands/onboard.js
CHANGED
|
@@ -101,7 +101,7 @@ Examples:
|
|
|
101
101
|
|
|
102
102
|
console.log('');
|
|
103
103
|
output.info('Step 1/2: Installing tool bundle');
|
|
104
|
-
const ok = await runSetupAllBundle(apiKey, {
|
|
104
|
+
const { ok, skipped } = await runSetupAllBundle(apiKey, {
|
|
105
105
|
backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
|
|
106
106
|
});
|
|
107
107
|
if (!ok) return;
|
|
@@ -127,7 +127,9 @@ Examples:
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
console.log('');
|
|
130
|
-
output.success(
|
|
130
|
+
output.success(skipped && skipped.length
|
|
131
|
+
? 'Onboarding complete — tools managed by MDM were skipped (see above)'
|
|
132
|
+
: 'Onboarding complete');
|
|
131
133
|
} catch (err) {
|
|
132
134
|
if (!err.displayed) output.error(err.message);
|
|
133
135
|
if (discoverySucceeded && opts.setCron) {
|
|
@@ -237,7 +239,7 @@ Examples:
|
|
|
237
239
|
|
|
238
240
|
console.log('');
|
|
239
241
|
output.info('Step 1/2: Installing MDM tool bundle');
|
|
240
|
-
const ok = await runMdmSetupAllBundle(adminApiKey, {
|
|
242
|
+
const { ok, skipped } = await runMdmSetupAllBundle(adminApiKey, {
|
|
241
243
|
backendUrl, gatewayUrl, backfill: !!opts.backfill,
|
|
242
244
|
});
|
|
243
245
|
if (!ok) return;
|
|
@@ -249,7 +251,9 @@ Examples:
|
|
|
249
251
|
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: discoveryDomain });
|
|
250
252
|
|
|
251
253
|
console.log('');
|
|
252
|
-
output.success(
|
|
254
|
+
output.success(skipped && skipped.length
|
|
255
|
+
? 'MDM onboarding complete — tools managed by MDM were skipped (see above)'
|
|
256
|
+
: 'MDM onboarding complete');
|
|
253
257
|
} catch (err) {
|
|
254
258
|
if (!err.displayed) output.error(err.message);
|
|
255
259
|
if (setupSucceeded) {
|
package/src/commands/setup.js
CHANGED
|
@@ -11,6 +11,14 @@ const { confirm } = require('../utils');
|
|
|
11
11
|
|
|
12
12
|
const SETUP_BASE_URL = 'https://raw.githubusercontent.com/websentry-ai/setup/refs/heads/main';
|
|
13
13
|
|
|
14
|
+
// A setup script exits with this code when it detects an existing MDM (managed)
|
|
15
|
+
// install for that tool and skips itself. It is a deliberate, non-fatal signal —
|
|
16
|
+
// not a failure — so the CLI suppresses the script's own output and reports the
|
|
17
|
+
// skip cleanly instead. Must stay in sync with the setup repo's setup.py scripts.
|
|
18
|
+
// Only emitted by setup runs: the scripts run the MDM check AFTER the --clear
|
|
19
|
+
// branch, so clear/nuke operations never produce this code.
|
|
20
|
+
const EXIT_MDM_PRESENT = 3;
|
|
21
|
+
|
|
14
22
|
// WSL reports as Linux via uname; only native Windows (cmd.exe / PowerShell)
|
|
15
23
|
// takes the Windows code path. WSL keeps using the Linux curl|python3 pipe.
|
|
16
24
|
function isWindowsNative() {
|
|
@@ -200,7 +208,9 @@ async function runPythonScriptWindows(scriptPath, args, { capture }) {
|
|
|
200
208
|
await downloadToFile(url, tmp);
|
|
201
209
|
const py = resolveWindowsPython();
|
|
202
210
|
try {
|
|
203
|
-
await
|
|
211
|
+
// `return await` so the resolved value (e.g. { mdmSkipped: true }) reaches
|
|
212
|
+
// the caller; the finally below still runs before the function returns.
|
|
213
|
+
return await new Promise((resolve, reject) => {
|
|
204
214
|
const child = spawn(py.cmd, [...py.prefix, tmp, ...parsePosixArgs(args)], {
|
|
205
215
|
stdio: capture ? ['pipe', 'pipe', 'pipe'] : 'inherit',
|
|
206
216
|
shell: false,
|
|
@@ -215,6 +225,8 @@ async function runPythonScriptWindows(scriptPath, args, { capture }) {
|
|
|
215
225
|
}
|
|
216
226
|
child.on('close', (code) => {
|
|
217
227
|
if (code === 0) return resolve();
|
|
228
|
+
// Managed by MDM — drop any captured output and signal a skip.
|
|
229
|
+
if (code === EXIT_MDM_PRESENT) return resolve({ mdmSkipped: true });
|
|
218
230
|
const err = new Error(out.trim() || `Setup script failed with exit code ${code}`);
|
|
219
231
|
if (capture) err.setupOutput = out.trim();
|
|
220
232
|
reject(err);
|
|
@@ -276,12 +288,14 @@ async function runSetupScript(scriptPath, apiKey, { clear = false, backendUrl, f
|
|
|
276
288
|
const args = buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, backfill });
|
|
277
289
|
console.log('');
|
|
278
290
|
if (isWindowsNative()) {
|
|
279
|
-
|
|
280
|
-
return;
|
|
291
|
+
return runPythonScriptWindows(scriptPath, args, { capture: false });
|
|
281
292
|
}
|
|
282
293
|
try {
|
|
283
294
|
execSync(buildSetupCommand(scriptPath, args), { stdio: 'inherit' });
|
|
284
295
|
} catch (err) {
|
|
296
|
+
// Managed by MDM — the script already printed its one-line notice (stdio is
|
|
297
|
+
// inherited here). Signal a skip so the caller can add the CLI notice.
|
|
298
|
+
if (err.status === EXIT_MDM_PRESENT) return { mdmSkipped: true };
|
|
285
299
|
throw new Error(`Setup script failed with exit code ${err.status || 1}. Check the output above for details.`);
|
|
286
300
|
}
|
|
287
301
|
}
|
|
@@ -309,6 +323,9 @@ function runScriptPiped(scriptPath, args) {
|
|
|
309
323
|
child.on('close', (code) => {
|
|
310
324
|
if (code === 0) {
|
|
311
325
|
resolve();
|
|
326
|
+
} else if (code === EXIT_MDM_PRESENT) {
|
|
327
|
+
// Managed by MDM — drop the captured script output and signal a skip.
|
|
328
|
+
resolve({ mdmSkipped: true });
|
|
312
329
|
} else {
|
|
313
330
|
const err = new Error(captured.trim() || `Setup failed with exit code ${code}`);
|
|
314
331
|
err.setupOutput = captured.trim();
|
|
@@ -350,24 +367,61 @@ function hasRootPrivileges() {
|
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
370
|
+
* Prints a clear, red, end-of-run notice for tools that were skipped because an
|
|
371
|
+
* MDM (organization-managed) setup is already present. Kept separate so callers
|
|
372
|
+
* can surface it at the very end of their flow.
|
|
373
|
+
*/
|
|
374
|
+
function reportMdmSkips(labels) {
|
|
375
|
+
if (!labels || labels.length === 0) return;
|
|
376
|
+
console.error('');
|
|
377
|
+
console.error(output.colors.bold(output.colors.red(
|
|
378
|
+
`✗ Skipped — managed by your organization (MDM): ${labels.join(', ')}`)));
|
|
379
|
+
console.error(output.colors.red(" User-level setup can't override MDM. Contact your IT admin to change it."));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Runs a batch of tools sequentially with spinners. Stops on the first hard
|
|
384
|
+
* failure. A tool that reports an MDM skip is NOT a failure — it's collected and
|
|
385
|
+
* surfaced (in red) at the end, and the batch continues. Returns
|
|
386
|
+
* { ok, skipped } — ok is false only on a hard failure; skipped is the list of
|
|
387
|
+
* MDM-managed tool labels so callers can qualify their own success message.
|
|
388
|
+
* When `summary` is set, a green success line is printed for the configured
|
|
389
|
+
* tools before the MDM notice.
|
|
355
390
|
*/
|
|
356
|
-
async function runBatch(tools, runFn, { clear = false } = {}) {
|
|
391
|
+
async function runBatch(tools, runFn, { clear = false, summary = null } = {}) {
|
|
357
392
|
const action = clear ? 'Clearing' : 'Setting up';
|
|
393
|
+
const mdmSkipped = [];
|
|
358
394
|
for (const tool of tools) {
|
|
359
395
|
const s = output.spinner(`${action} ${tool.label}...`);
|
|
360
396
|
try {
|
|
361
|
-
await runFn(tool);
|
|
362
|
-
|
|
397
|
+
const result = await runFn(tool);
|
|
398
|
+
if (result && result.mdmSkipped) {
|
|
399
|
+
// Stop the spinner and leave an inline marker so the tool doesn't just
|
|
400
|
+
// vanish mid-batch; the actionable red summary still prints at the end.
|
|
401
|
+
s.stop();
|
|
402
|
+
console.error(output.colors.dim(` Skipped ${tool.label} — managed by MDM`));
|
|
403
|
+
mdmSkipped.push(tool.label);
|
|
404
|
+
} else {
|
|
405
|
+
s.succeed(tool.label);
|
|
406
|
+
}
|
|
363
407
|
} catch (err) {
|
|
364
408
|
s.fail(`Failed: ${tool.label}`);
|
|
365
409
|
if (err.setupOutput) console.error('\n' + err.setupOutput);
|
|
366
410
|
process.exitCode = 1;
|
|
367
|
-
|
|
411
|
+
// Still surface any tools skipped before this failure so the notice isn't lost.
|
|
412
|
+
reportMdmSkips(mdmSkipped);
|
|
413
|
+
return { ok: false, skipped: mdmSkipped };
|
|
368
414
|
}
|
|
369
415
|
}
|
|
370
|
-
|
|
416
|
+
const configured = tools.length - mdmSkipped.length;
|
|
417
|
+
if (summary && configured > 0) {
|
|
418
|
+
console.log('');
|
|
419
|
+
// Some tools may have been MDM-managed, so report the actual count and the
|
|
420
|
+
// matching verb. (clear never produces skips, but keep the verb consistent.)
|
|
421
|
+
output.success(mdmSkipped.length ? `${configured} of ${tools.length} tools ${clear ? 'cleared' : 'configured'}` : summary);
|
|
422
|
+
}
|
|
423
|
+
reportMdmSkips(mdmSkipped);
|
|
424
|
+
return { ok: true, skipped: mdmSkipped };
|
|
371
425
|
}
|
|
372
426
|
|
|
373
427
|
/**
|
|
@@ -468,6 +522,11 @@ Examples:
|
|
|
468
522
|
When setting up, if you are not logged in and --api-key is not provided, the
|
|
469
523
|
browser opens automatically to authenticate first. Clearing (--clear) never
|
|
470
524
|
requires authentication.
|
|
525
|
+
|
|
526
|
+
If an MDM (organization-managed) setup is already present for a tool, user-level
|
|
527
|
+
setup for that tool is skipped automatically — the managed configuration already
|
|
528
|
+
enforces Unbound for every user on the device. To change it, an administrator
|
|
529
|
+
must update the MDM configuration.
|
|
471
530
|
`)
|
|
472
531
|
.action(async (tools, opts) => {
|
|
473
532
|
try {
|
|
@@ -527,18 +586,14 @@ requires authentication.
|
|
|
527
586
|
if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
|
|
528
587
|
}
|
|
529
588
|
}
|
|
530
|
-
|
|
589
|
+
await runBatch(selectedTools, (tool) => {
|
|
531
590
|
const toolArgs = buildScriptArgs(apiKey, {
|
|
532
591
|
...urlOpts,
|
|
533
592
|
clear: opts.clear,
|
|
534
593
|
backfill: opts.backfill && scriptSupportsBackfill(tool.script),
|
|
535
594
|
});
|
|
536
595
|
return runScriptPiped(tool.script, toolArgs);
|
|
537
|
-
}, { clear: opts.clear });
|
|
538
|
-
if (!ok) return;
|
|
539
|
-
|
|
540
|
-
console.log('');
|
|
541
|
-
output.success(opts.clear ? 'All tools cleared' : 'All tools configured');
|
|
596
|
+
}, { clear: opts.clear, summary: opts.clear ? 'All tools cleared' : 'All tools configured' });
|
|
542
597
|
return;
|
|
543
598
|
}
|
|
544
599
|
|
|
@@ -604,7 +659,8 @@ requires authentication.
|
|
|
604
659
|
const { script, label } = SETUP_TOOL_MAP[toolName];
|
|
605
660
|
const backfill = opts.backfill && scriptSupportsBackfill(script);
|
|
606
661
|
if (opts.backfill && !backfill) noteBackfillUnsupported(label, script);
|
|
607
|
-
await runSetupScript(script, apiKey, { clear: opts.clear, backfill, ...urlOpts });
|
|
662
|
+
const r = await runSetupScript(script, apiKey, { clear: opts.clear, backfill, ...urlOpts });
|
|
663
|
+
if (r && r.mdmSkipped) reportMdmSkips([label]);
|
|
608
664
|
} else if (MODE_TOOLS[toolName]) {
|
|
609
665
|
const mode = MODE_TOOLS[toolName];
|
|
610
666
|
if (opts.clear) {
|
|
@@ -621,7 +677,8 @@ requires authentication.
|
|
|
621
677
|
const { script, label } = SETUP_TOOL_MAP[resolved];
|
|
622
678
|
const backfill = opts.backfill && scriptSupportsBackfill(script);
|
|
623
679
|
if (opts.backfill && !backfill) noteBackfillUnsupported(label, script);
|
|
624
|
-
await runSetupScript(script, apiKey, { ...urlOpts, backfill });
|
|
680
|
+
const r = await runSetupScript(script, apiKey, { ...urlOpts, backfill });
|
|
681
|
+
if (r && r.mdmSkipped) reportMdmSkips([label]);
|
|
625
682
|
}
|
|
626
683
|
} else if (INSTRUCTION_TOOLS[toolName]) {
|
|
627
684
|
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
|
|
@@ -664,14 +721,14 @@ requires authentication.
|
|
|
664
721
|
if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
|
|
665
722
|
}
|
|
666
723
|
}
|
|
667
|
-
const ok = await runBatch(resolvedScripts, (tool) => {
|
|
724
|
+
const { ok } = await runBatch(resolvedScripts, (tool) => {
|
|
668
725
|
const toolArgs = buildScriptArgs(apiKey, {
|
|
669
726
|
...urlOpts,
|
|
670
727
|
clear: opts.clear,
|
|
671
728
|
backfill: opts.backfill && scriptSupportsBackfill(tool.script),
|
|
672
729
|
});
|
|
673
730
|
return runScriptPiped(tool.script, toolArgs);
|
|
674
|
-
}, { clear: opts.clear });
|
|
731
|
+
}, { clear: opts.clear, summary: opts.clear ? 'All tools cleared' : 'All tools configured' });
|
|
675
732
|
if (!ok) return;
|
|
676
733
|
}
|
|
677
734
|
|
|
@@ -682,10 +739,6 @@ requires authentication.
|
|
|
682
739
|
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
|
|
683
740
|
}
|
|
684
741
|
|
|
685
|
-
if (resolvedScripts.length > 0) {
|
|
686
|
-
console.log('');
|
|
687
|
-
output.success(opts.clear ? 'All tools cleared' : 'All tools configured');
|
|
688
|
-
}
|
|
689
742
|
} catch (err) {
|
|
690
743
|
if (err.message === 'Selection cancelled') return;
|
|
691
744
|
if (!err.displayed) output.error(err.message);
|
|
@@ -832,7 +885,7 @@ Clear examples (no API key required):
|
|
|
832
885
|
}
|
|
833
886
|
}
|
|
834
887
|
|
|
835
|
-
const ok = await runBatch(
|
|
888
|
+
const { ok } = await runBatch(
|
|
836
889
|
resolvedTools,
|
|
837
890
|
(tool) => {
|
|
838
891
|
const toolArgs = buildScriptArgs(adminApiKey, {
|
|
@@ -844,12 +897,9 @@ Clear examples (no API key required):
|
|
|
844
897
|
});
|
|
845
898
|
return runScriptPiped(tool.script, toolArgs);
|
|
846
899
|
},
|
|
847
|
-
{ clear: globalOpts.clear }
|
|
900
|
+
{ clear: globalOpts.clear, summary: globalOpts.clear ? 'All tools cleared' : 'All tools configured' }
|
|
848
901
|
);
|
|
849
902
|
if (!ok) return;
|
|
850
|
-
|
|
851
|
-
console.log('');
|
|
852
|
-
output.success(globalOpts.clear ? 'All tools cleared' : 'All tools configured');
|
|
853
903
|
} catch (err) {
|
|
854
904
|
output.error(err.message);
|
|
855
905
|
process.exitCode = 1;
|
package/src/output.js
CHANGED
|
@@ -349,4 +349,4 @@ function multiSelect(message, options) {
|
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
module.exports = { table, json, keyValue, success, error, warn, info, spinner, select, multiSelect };
|
|
352
|
+
module.exports = { table, json, keyValue, success, error, warn, info, spinner, select, multiSelect, colors: c };
|