unbound-cli 1.3.0 → 1.3.2
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
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -16,7 +16,7 @@ const DISCOVERY_EXIT_UNSUPPORTED_OS = 3;
|
|
|
16
16
|
// indefinitely. Discovery enforces this itself — on expiry it releases its lock,
|
|
17
17
|
// reports the run as failed, and exits non-zero. Kept in sync with
|
|
18
18
|
// setup/mdm/onboard.py's DISCOVERY_TIMEOUT_SECONDS.
|
|
19
|
-
const DISCOVERY_TIMEOUT_SECONDS =
|
|
19
|
+
const DISCOVERY_TIMEOUT_SECONDS = 5400;
|
|
20
20
|
|
|
21
21
|
// Classifies a discovery subprocess exit code:
|
|
22
22
|
// 'success' (scan ran), 'unsupported' (skipped on this OS), or 'failure'.
|
package/src/commands/policy.js
CHANGED
|
@@ -311,6 +311,16 @@ function parseFieldSpec(spec) {
|
|
|
311
311
|
return [spec.slice(0, eq), spec.slice(eq + 1)];
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// "ANY" is a wildcard across all fields; it can't be ANDed with other conditions.
|
|
315
|
+
// Mirrors the backend rule so the CLI fails fast before the API call. The check is
|
|
316
|
+
// case-insensitive so `--field Any=*`/`--field any=*` are caught too.
|
|
317
|
+
function assertAnyExclusive(configObj) {
|
|
318
|
+
const keys = Object.keys(configObj || {});
|
|
319
|
+
if (keys.length > 1 && keys.some((k) => k.toUpperCase() === 'ANY')) {
|
|
320
|
+
throw new Error("The 'ANY' field cannot be combined with other --field conditions (ANY already matches every field).");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
314
324
|
/**
|
|
315
325
|
* Convert a `--sub-type` flag (kebab-case) to the backend's snake_case form.
|
|
316
326
|
*/
|
|
@@ -1421,7 +1431,7 @@ Subcommands:
|
|
|
1421
1431
|
.description('Create a TERMINAL_COMMAND policy: monitor or block shell commands run by AI coding tools.')
|
|
1422
1432
|
.requiredOption('--name <name>', 'Policy name (required)')
|
|
1423
1433
|
.requiredOption('--command-family <family>', 'Command family (e.g. git, filesystem, network). See `policy tool families`.')
|
|
1424
|
-
.option('--field <key=pattern>', 'Match field: <key>=<pattern>. Repeatable. At least one required unless --config is used.', (val, prev = []) => [...prev, val])
|
|
1434
|
+
.option('--field <key=pattern>', 'Match field: <key>=<pattern>. Repeatable — multiple --field are ANDed (all must match), one per field, for a more specific policy. At least one required unless --config is used.', (val, prev = []) => [...prev, val])
|
|
1425
1435
|
.requiredOption('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
|
|
1426
1436
|
.option('--description <text>', 'Human-readable description')
|
|
1427
1437
|
.option('--custom-message <text>', 'Message shown to the user when the policy fires. Required when --action is BLOCK or WARN.')
|
|
@@ -1438,9 +1448,11 @@ Examples:
|
|
|
1438
1448
|
$ unbound policy tool create-terminal --name "Audit git push" \\
|
|
1439
1449
|
--command-family git --field command='git push*' --action AUDIT
|
|
1440
1450
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
--
|
|
1451
|
+
# Multiple --field are ANDed — this fires only on a push to main on origin:
|
|
1452
|
+
$ unbound policy tool create-terminal --name "Block push to main on origin" \\
|
|
1453
|
+
--command-family git_action \\
|
|
1454
|
+
--field operation=push --field branch=main --field remote='*origin*' \\
|
|
1455
|
+
--action BLOCK --custom-message "Pushing to main on origin is blocked."
|
|
1444
1456
|
|
|
1445
1457
|
Discover valid command families and their fields: unbound policy tool families
|
|
1446
1458
|
Learn more: ${DOCS_TOOL}
|
|
@@ -1469,6 +1481,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1469
1481
|
configObj[k] = v;
|
|
1470
1482
|
}
|
|
1471
1483
|
}
|
|
1484
|
+
assertAnyExclusive(configObj);
|
|
1472
1485
|
|
|
1473
1486
|
const body = {
|
|
1474
1487
|
name: opts.name,
|
|
@@ -1595,7 +1608,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1595
1608
|
.option('--enabled', 'Enable the policy')
|
|
1596
1609
|
.option('--disabled', 'Disable the policy')
|
|
1597
1610
|
.option('--command-family <family>', '(TERMINAL only) new command family')
|
|
1598
|
-
.option('--field <key=pattern>', '(TERMINAL only) replace config
|
|
1611
|
+
.option('--field <key=pattern>', '(TERMINAL only) replace config fields. Repeatable — multiple --field are ANDed (all must match), one per field.', (val, prev = []) => [...prev, val])
|
|
1599
1612
|
.option('--mcp-server <server>', '(MCP only) new MCP server')
|
|
1600
1613
|
.option('--mcp-tool <tool>', '(MCP only) new MCP tool')
|
|
1601
1614
|
.option('--mcp-action-type <type>', '(MCP only) new MCP action type')
|
|
@@ -1627,6 +1640,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1627
1640
|
} else if (opts.config) {
|
|
1628
1641
|
body.config = parseJsonOrThrow(opts.config);
|
|
1629
1642
|
}
|
|
1643
|
+
if (body.config) assertAnyExclusive(body.config);
|
|
1630
1644
|
|
|
1631
1645
|
if (opts.mcpServer) body.mcp_server = opts.mcpServer;
|
|
1632
1646
|
if (opts.mcpTool) body.mcp_tool = opts.mcpTool;
|
|
@@ -1888,4 +1902,4 @@ Learn more: ${DOCS_POLICIES}
|
|
|
1888
1902
|
registerLegacy(policy);
|
|
1889
1903
|
}
|
|
1890
1904
|
|
|
1891
|
-
module.exports = { register };
|
|
1905
|
+
module.exports = { register, assertAnyExclusive };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
|
|
6
|
+
const { assertAnyExclusive } = require('../src/commands/policy');
|
|
7
|
+
|
|
8
|
+
test('assertAnyExclusive: allows a single ANY condition', () => {
|
|
9
|
+
assert.doesNotThrow(() => assertAnyExclusive({ ANY: '*' }));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('assertAnyExclusive: allows multiple non-ANY fields (AND)', () => {
|
|
13
|
+
assert.doesNotThrow(() => assertAnyExclusive({ path: '/etc/*', operation: 'write' }));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('assertAnyExclusive: rejects ANY combined with another field', () => {
|
|
17
|
+
assert.throws(() => assertAnyExclusive({ ANY: '*', path: '/etc/*' }), /ANY.*cannot be combined/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('assertAnyExclusive: case-insensitive — rejects lowercase/mixed-case any with another field', () => {
|
|
21
|
+
assert.throws(() => assertAnyExclusive({ any: '*', path: '/etc/*' }), /ANY.*cannot be combined/);
|
|
22
|
+
assert.throws(() => assertAnyExclusive({ Any: '*', path: '/etc/*' }), /ANY.*cannot be combined/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('assertAnyExclusive: tolerates empty/undefined config', () => {
|
|
26
|
+
assert.doesNotThrow(() => assertAnyExclusive({}));
|
|
27
|
+
assert.doesNotThrow(() => assertAnyExclusive(undefined));
|
|
28
|
+
});
|