unbound-cli 1.16.23 → 1.16.25
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
|
@@ -41,6 +41,12 @@ const DATASETS = {
|
|
|
41
41
|
summary: 'MCP calls to servers your org has not sanctioned (blocked or warned).',
|
|
42
42
|
tableColumns: ['Timestamp', 'User', 'MCP Server', 'MCP Tool', 'Status'],
|
|
43
43
|
},
|
|
44
|
+
unofficial: {
|
|
45
|
+
key: 'unofficial',
|
|
46
|
+
title: 'Unofficial MCP Usage',
|
|
47
|
+
summary: 'MCP calls blocked because the publisher is not an official one.',
|
|
48
|
+
tableColumns: ['Timestamp', 'User', 'MCP Server', 'MCP Tool', 'Status'],
|
|
49
|
+
},
|
|
44
50
|
'mcp-servers': {
|
|
45
51
|
key: 'mcp-servers',
|
|
46
52
|
title: 'MCP Servers',
|
|
@@ -272,6 +278,7 @@ DATASETS
|
|
|
272
278
|
terminal ${DATASETS.terminal.summary}
|
|
273
279
|
mcp-actions ${DATASETS['mcp-actions'].summary}
|
|
274
280
|
unsanctioned ${DATASETS.unsanctioned.summary}
|
|
281
|
+
unofficial ${DATASETS.unofficial.summary}
|
|
275
282
|
mcp-servers ${DATASETS['mcp-servers'].summary}
|
|
276
283
|
|
|
277
284
|
Examples:
|
|
@@ -326,6 +333,21 @@ Examples:
|
|
|
326
333
|
$ unbound analytics tool-use unsanctioned --format csv -o unsanctioned.csv
|
|
327
334
|
${FILTER_HELP}${AGENT_HELP}`, { server: true });
|
|
328
335
|
|
|
336
|
+
attach(toolUse, 'unofficial', DATASETS.unofficial, `
|
|
337
|
+
Columns:
|
|
338
|
+
Timestamp, User, Email, AI Tool, MCP Server, MCP Tool,
|
|
339
|
+
Status, Sanction Message, User Prompt
|
|
340
|
+
|
|
341
|
+
A narrower slice of unsanctioned: only the calls blocked because the MCP
|
|
342
|
+
publisher is not an official one. No dashboard page shows this on its own,
|
|
343
|
+
so the CLI and the API are the only ways to read it.
|
|
344
|
+
|
|
345
|
+
Examples:
|
|
346
|
+
$ unbound analytics tool-use unofficial
|
|
347
|
+
$ unbound analytics tool-use unofficial --time-window THIRTY_DAYS
|
|
348
|
+
$ unbound analytics tool-use unofficial --format csv -o unofficial.csv
|
|
349
|
+
${FILTER_HELP}${AGENT_HELP}`, { server: true });
|
|
350
|
+
|
|
329
351
|
attach(toolUse, 'mcp-servers', DATASETS['mcp-servers'], `
|
|
330
352
|
Columns:
|
|
331
353
|
MCP Server, Actions, Average Risk Score
|
package/src/commands/download.js
CHANGED
|
@@ -57,6 +57,36 @@ FILTERS
|
|
|
57
57
|
than failing — 'tool_type=CURSOR' returns every tool. An invalid VALUE for a
|
|
58
58
|
known key IS rejected, with the supported values listed. Run --check first and
|
|
59
59
|
confirm the count moved.
|
|
60
|
+
|
|
61
|
+
FILTER VALUES
|
|
62
|
+
Most filters take IDs or names. These take a fixed vocabulary:
|
|
63
|
+
|
|
64
|
+
weeks ai-usage kinds only: 4, 8 or 12 (default 8). These cover
|
|
65
|
+
whole weeks, so --last and --start/--end do not apply.
|
|
66
|
+
status ai-usage-users-export: active | silent
|
|
67
|
+
band ai-usage-teams-export: high | mid | low
|
|
68
|
+
trend ai-usage-teams-export: up | down | flat
|
|
69
|
+
enforcement users-export, analytics-tasks-export. Repeatable:
|
|
70
|
+
data:audit, data:block, mcp:audit, mcp:block, mcp:warn,
|
|
71
|
+
mcp:approval, terminal:audit, terminal:block,
|
|
72
|
+
terminal:warn, terminal:approval, skills:inject
|
|
73
|
+
policy users-export, analytics-tasks-export: cmd:<id>, data:<id>,
|
|
74
|
+
finding:<name>, sanction:unsanctioned, sanction:unofficial
|
|
75
|
+
sort_field users-export: total_cost (default), total_tokens,
|
|
76
|
+
task_count, avg_cost_per_task, cache_rate, models_used,
|
|
77
|
+
policy_enforcements
|
|
78
|
+
risk_level discovery-risky-users-export. Repeatable:
|
|
79
|
+
low | medium | high | critical
|
|
80
|
+
An unrecognised value is DROPPED, not rejected, so a typo
|
|
81
|
+
exports every risk level. Confirm with --check.
|
|
82
|
+
autonomy_level discovery-risky-users-export. Repeatable:
|
|
83
|
+
read_only | ask_first | auto_edit | full_auto
|
|
84
|
+
Dropped on a typo, same as risk_level above.
|
|
85
|
+
columns discovery-risky-users-export. Repeatable; omit for the
|
|
86
|
+
default seven. user, email, system_user, device_id,
|
|
87
|
+
last_sync, risk_score, risk_level, risk_factors,
|
|
88
|
+
autonomy_level, tools, tool_count, permission_risk,
|
|
89
|
+
config_file_risk. The file keeps that order whatever you ask for.
|
|
60
90
|
`;
|
|
61
91
|
|
|
62
92
|
/** `--last 7d` / `--last 24h` -> an absolute window the server understands. */
|
|
@@ -201,8 +231,12 @@ Examples:
|
|
|
201
231
|
$ unbound download start logs-tasks-export --start 2026-07-01 --end 2026-07-31
|
|
202
232
|
$ unbound download start users-export --last 30d
|
|
203
233
|
$ unbound download start analytics-export --filter dataset=terminal-runs --last 7d
|
|
234
|
+
$ unbound download start ai-usage-teams-export --filter weeks=12
|
|
204
235
|
$ unbound download start logs-export --check --last 90d --json
|
|
205
236
|
|
|
237
|
+
The two ai-usage kinds cover whole weeks, not a date range: pass
|
|
238
|
+
'--filter weeks=4|8|12' (default 8). --last and --start/--end do not apply.
|
|
239
|
+
|
|
206
240
|
Admin or Analytics Viewer. Exports are kept for ${RETENTION_HOURS} hours.
|
|
207
241
|
${FILTER_HELP}${AGENT_HELP}`)
|
|
208
242
|
.action(async (kind, opts) => {
|
package/src/lib/downloads.js
CHANGED
|
@@ -35,24 +35,44 @@ const KINDS = {
|
|
|
35
35
|
'logs-export': {
|
|
36
36
|
summary: 'Every LLM call: prompt, model, tokens, status, user and task ID.',
|
|
37
37
|
filters: ['model_ids', 'application_ids', 'status_codes', 'application_owner_email',
|
|
38
|
-
'application_types', 'tool_types'],
|
|
38
|
+
'application_types', 'tool_types', 'search'],
|
|
39
39
|
},
|
|
40
40
|
'logs-tasks-export': {
|
|
41
41
|
summary: 'One row per task (agent session), with its thread ID.',
|
|
42
|
-
filters: ['model_ids', 'tool_types', 'application_owner_email', 'user_ids'],
|
|
42
|
+
filters: ['model_ids', 'tool_types', 'application_owner_email', 'user_ids', 'search'],
|
|
43
43
|
},
|
|
44
44
|
'analytics-export': {
|
|
45
|
-
summary: 'Tool Use rows. Needs dataset=terminal-runs|mcp-actions|unsanctioned|mcp-servers.',
|
|
46
|
-
filters: ['dataset', 'risk_levels', 'command_families', '
|
|
47
|
-
'
|
|
45
|
+
summary: 'Tool Use rows. Needs dataset=terminal-runs|mcp-actions|unsanctioned|unofficial|mcp-servers.',
|
|
46
|
+
filters: ['dataset', 'risk_levels', 'command_families', 'policy_ids', 'policy_actions',
|
|
47
|
+
'server', 'server_name', 'canonical_group_id', 'grouped', 'intent_attribution',
|
|
48
|
+
'has_policy_match', 'application_types', 'application_ids', 'application_owner_email',
|
|
49
|
+
'tool_types', 'user_ids', 'department_names', 'manager_names', 'search',
|
|
50
|
+
'sort_field', 'sort_order'],
|
|
48
51
|
},
|
|
49
52
|
'analytics-tasks-export': {
|
|
50
53
|
summary: 'Analytics > Tasks, optionally narrowed to one heatmap day.',
|
|
51
|
-
filters: ['user_ids', 'tool_types', 'date', 'tz'
|
|
54
|
+
filters: ['user_ids', 'department_names', 'manager_names', 'tool_types', 'date', 'tz',
|
|
55
|
+
'enforcement', 'policy'],
|
|
52
56
|
},
|
|
53
57
|
'users-export': {
|
|
54
58
|
summary: 'Analytics > Users: per-user cost, tokens and activity.',
|
|
55
|
-
filters: ['user_ids', '
|
|
59
|
+
filters: ['user_ids', 'department_names', 'manager_names', 'tool_types', 'search',
|
|
60
|
+
'enforcement', 'policy', 'sort_field', 'sort_order'],
|
|
61
|
+
},
|
|
62
|
+
'ai-usage-teams-export': {
|
|
63
|
+
summary: 'Analytics > Usage: weekly AI adoption per team, one column per week.',
|
|
64
|
+
filters: ['weeks', 'department_names', 'manager_names', 'search', 'band', 'trend',
|
|
65
|
+
'group_by', 'sort_by', 'sort_dir'],
|
|
66
|
+
},
|
|
67
|
+
'ai-usage-users-export': {
|
|
68
|
+
summary: 'Analytics > Usage: the people inside a team. Needs department_names.',
|
|
69
|
+
filters: ['weeks', 'department_names', 'manager_names', 'search', 'status',
|
|
70
|
+
'sort_by', 'sort_dir'],
|
|
71
|
+
},
|
|
72
|
+
'discovery-risky-users-export': {
|
|
73
|
+
summary: 'AI Tools Discovery > Users Risk Leaderboard. Pick your own columns.',
|
|
74
|
+
filters: ['search', 'filter', 'high_risk', 'risk_level', 'autonomy_level', 'tool',
|
|
75
|
+
'sort_by', 'sort_direction', 'columns'],
|
|
56
76
|
},
|
|
57
77
|
};
|
|
58
78
|
|
|
@@ -48,6 +48,9 @@ test('every dataset names only columns the export actually returns', () => {
|
|
|
48
48
|
'Targets', 'User Prompt', 'Risk Score', 'Risk Reason', 'Initiator', 'Matched Policies'],
|
|
49
49
|
unsanctioned: ['Timestamp', 'User', 'Email', 'AI Tool', 'MCP Server', 'MCP Tool',
|
|
50
50
|
'Status', 'Sanction Message', 'User Prompt'],
|
|
51
|
+
// The server builds unofficial from the same column set as unsanctioned.
|
|
52
|
+
unofficial: ['Timestamp', 'User', 'Email', 'AI Tool', 'MCP Server', 'MCP Tool',
|
|
53
|
+
'Status', 'Sanction Message', 'User Prompt'],
|
|
51
54
|
'mcp-servers': ['MCP Server', 'Actions', 'Average Risk Score'],
|
|
52
55
|
};
|
|
53
56
|
|
|
@@ -66,3 +69,16 @@ test('every dataset names only columns the export actually returns', () => {
|
|
|
66
69
|
test('--json works when --format was never passed', () => {
|
|
67
70
|
assert.strictEqual(resolveFormat({ json: true, format: undefined }), 'json');
|
|
68
71
|
});
|
|
72
|
+
|
|
73
|
+
test('every Tool Use dataset the server has is reachable as a subcommand', () => {
|
|
74
|
+
// 'unofficial' has no dashboard page, so the CLI and the API are the only
|
|
75
|
+
// ways to read it; dropping it here makes it unreachable rather than awkward.
|
|
76
|
+
assert.deepEqual(Object.keys(DATASETS), [
|
|
77
|
+
'terminal', 'mcp-actions', 'unsanctioned', 'unofficial', 'mcp-servers',
|
|
78
|
+
]);
|
|
79
|
+
for (const [name, meta] of Object.entries(DATASETS)) {
|
|
80
|
+
assert.ok(meta.key, `${name} needs the server's dataset key`);
|
|
81
|
+
assert.ok(meta.summary && meta.summary.length > 10, `${name} needs a summary`);
|
|
82
|
+
assert.ok(meta.tableColumns.length > 0, `${name} needs table columns`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -69,6 +69,44 @@ test('every advertised kind names the filters it accepts', () => {
|
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
test('every kind the backend serves is listed', () => {
|
|
73
|
+
// Literal list, in the order help prints them. The backend registry cannot be
|
|
74
|
+
// queried, so a kind added there is only discoverable once it is added here
|
|
75
|
+
// too — this is what makes that omission a failing test rather than silence.
|
|
76
|
+
assert.deepEqual(Object.keys(KINDS), [
|
|
77
|
+
'logs-export',
|
|
78
|
+
'logs-tasks-export',
|
|
79
|
+
'analytics-export',
|
|
80
|
+
'analytics-tasks-export',
|
|
81
|
+
'users-export',
|
|
82
|
+
'ai-usage-teams-export',
|
|
83
|
+
'ai-usage-users-export',
|
|
84
|
+
'discovery-risky-users-export',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('the filters a kind advertises include the ones that change what is exported', () => {
|
|
89
|
+
// An unknown filter key is ignored by the server rather than rejected, so a
|
|
90
|
+
// missing entry here reads to the caller as "this kind cannot be narrowed"
|
|
91
|
+
// and they take a wider file than they asked for.
|
|
92
|
+
const required = {
|
|
93
|
+
'logs-export': ['search', 'tool_types'],
|
|
94
|
+
'logs-tasks-export': ['search', 'user_ids'],
|
|
95
|
+
'analytics-export': ['dataset', 'department_names', 'manager_names', 'search'],
|
|
96
|
+
'analytics-tasks-export': ['department_names', 'manager_names', 'enforcement', 'policy'],
|
|
97
|
+
'users-export': ['department_names', 'manager_names', 'tool_types', 'search',
|
|
98
|
+
'enforcement', 'policy'],
|
|
99
|
+
'ai-usage-teams-export': ['weeks', 'band', 'trend'],
|
|
100
|
+
'ai-usage-users-export': ['weeks', 'department_names', 'status'],
|
|
101
|
+
'discovery-risky-users-export': ['risk_level', 'autonomy_level', 'columns'],
|
|
102
|
+
};
|
|
103
|
+
for (const [kind, filters] of Object.entries(required)) {
|
|
104
|
+
for (const filter of filters) {
|
|
105
|
+
assert.ok(KINDS[kind].filters.includes(filter), `${kind} must advertise ${filter}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
72
110
|
test('isJobId accepts a UUID and rejects anything else', () => {
|
|
73
111
|
assert.ok(isJobId('4f2a1c9e-1b2c-4d5e-8f90-a1b2c3d4e5f6'));
|
|
74
112
|
assert.ok(isJobId('4F2A1C9E-1B2C-4D5E-8F90-A1B2C3D4E5F6'));
|