quoxcli 0.4.0 → 0.6.0
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/CHANGELOG.md +110 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +42 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +189 -34
- package/lib/quoxon/app.js +1277 -188
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +27 -6
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
package/lib/commands/mon.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* None of the three are org-scoped (single shared config/cache).
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { attachWazuhVerbs } from './wazuh.js';
|
|
28
29
|
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
29
30
|
import { parseJSON } from './helpers.js';
|
|
30
31
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
@@ -77,6 +78,17 @@ function reportDockerReadFailures(data) {
|
|
|
77
78
|
return false;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* AGH-chain reads answer 202 { status: 'computing' } while the collector builds them in the
|
|
83
|
+
* background; an empty body then is not data. Says so and returns true (caller stops).
|
|
84
|
+
*/
|
|
85
|
+
function reportComputing(program, data, message) {
|
|
86
|
+
if (!data || data.status !== 'computing') return false;
|
|
87
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return true; }
|
|
88
|
+
printWarning(message);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
export function registerMonCommands(program, ctx) {
|
|
81
93
|
const { client, auth } = ctx;
|
|
82
94
|
|
|
@@ -262,147 +274,48 @@ export function registerMonCommands(program, ctx) {
|
|
|
262
274
|
.command('wazuh')
|
|
263
275
|
.description('Wazuh security monitoring: manager status, agents, alerts, vulnerabilities');
|
|
264
276
|
|
|
265
|
-
wazuh
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
.command('agents')
|
|
277
|
-
.description('List Wazuh agents')
|
|
278
|
-
.option('--status <status>', 'filter by agent status')
|
|
279
|
-
.option('--limit <n>', 'max results')
|
|
280
|
-
.option('--offset <n>', 'result offset')
|
|
281
|
-
.option('--q <query>', 'query string passed through to the Wazuh API')
|
|
282
|
-
.action(async (options) => {
|
|
283
|
-
auth.requireAuth();
|
|
284
|
-
const ep = resolveEndpoint('mon.wazuh.agents.list');
|
|
285
|
-
const query = {};
|
|
286
|
-
if (options.status) query.status = options.status;
|
|
287
|
-
if (options.limit) query.limit = options.limit;
|
|
288
|
-
if (options.offset) query.offset = options.offset;
|
|
289
|
-
if (options.q) query.q = options.q;
|
|
290
|
-
const data = await client.get(ep.path, { service: ep.service, query });
|
|
291
|
-
const agents = data.data?.data?.affected_items || data.data?.affected_items || data.data || [];
|
|
292
|
-
formatOutput({
|
|
293
|
-
columns: ['id', 'name', 'ip', 'status', 'version'],
|
|
294
|
-
headers: ['ID', 'Name', 'IP', 'Status', 'Version'],
|
|
295
|
-
rows: Array.isArray(agents) ? agents : [],
|
|
296
|
-
}, { format: program.opts().output, emptyMessage: 'No Wazuh agents found.' });
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
wazuh
|
|
300
|
-
.command('alerts')
|
|
301
|
-
.description('Query recent Wazuh alerts')
|
|
302
|
-
.option('--q <query>', 'search text (rule description / agent name / rule groups)')
|
|
303
|
-
.option('--limit <n>', 'max results (default 50, capped at 500)')
|
|
304
|
-
.action(async (options) => {
|
|
305
|
-
auth.requireAuth();
|
|
306
|
-
const ep = resolveEndpoint('mon.wazuh.alerts.list');
|
|
307
|
-
const query = {};
|
|
308
|
-
if (options.q) query.q = options.q;
|
|
309
|
-
if (options.limit) query.limit = options.limit;
|
|
310
|
-
const data = await client.get(ep.path, { service: ep.service, query });
|
|
311
|
-
formatOutput({
|
|
312
|
-
columns: ['timestamp', 'rule', 'agent', 'level'],
|
|
313
|
-
headers: ['Time', 'Rule', 'Agent', 'Level'],
|
|
314
|
-
rows: (data.data || []).map(a => ({
|
|
315
|
-
timestamp: a.timestamp,
|
|
316
|
-
rule: a.rule?.description || '',
|
|
317
|
-
agent: a.agent?.name || '',
|
|
318
|
-
level: a.rule?.level ?? '',
|
|
319
|
-
})),
|
|
320
|
-
}, { format: program.opts().output, emptyMessage: 'No alerts found.' });
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
wazuh
|
|
324
|
-
.command('vulns')
|
|
325
|
-
.description('Query current vulnerability states')
|
|
326
|
-
.option('--agent-id <id>', 'filter by agent ID')
|
|
327
|
-
.option('--agent-name <name>', 'filter by agent name')
|
|
328
|
-
.option('--severity <severity>', 'filter by severity')
|
|
329
|
-
.option('--limit <n>', 'max results (default 100, capped at 1000)')
|
|
330
|
-
.action(async (options) => {
|
|
331
|
-
auth.requireAuth();
|
|
332
|
-
const ep = resolveEndpoint('mon.wazuh.vulnerabilities.list');
|
|
333
|
-
const query = {};
|
|
334
|
-
if (options.agentId) query.agent_id = options.agentId;
|
|
335
|
-
if (options.agentName) query.agent_name = options.agentName;
|
|
336
|
-
if (options.severity) query.severity = options.severity;
|
|
337
|
-
if (options.limit) query.limit = options.limit;
|
|
338
|
-
const data = await client.get(ep.path, { service: ep.service, query });
|
|
339
|
-
formatOutput({
|
|
340
|
-
columns: ['agent', 'cve', 'severity', 'score'],
|
|
341
|
-
headers: ['Agent', 'CVE', 'Severity', 'Score'],
|
|
342
|
-
rows: (data.data || []).map(v => ({
|
|
343
|
-
agent: v.agent?.name || '',
|
|
344
|
-
cve: v.vulnerability?.id || '',
|
|
345
|
-
severity: v.vulnerability?.severity || '',
|
|
346
|
-
score: v.vulnerability?.score?.base ?? '',
|
|
347
|
-
})),
|
|
348
|
-
}, { format: program.opts().output, emptyMessage: 'No vulnerabilities found.' });
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
wazuh
|
|
352
|
-
.command('deploy-history')
|
|
353
|
-
.description('Show Wazuh agent deploy history')
|
|
354
|
-
.option('--mode <mode>', 'latest|all', 'latest')
|
|
355
|
-
.option('--limit <n>', 'max results (mode=all only)', '200')
|
|
356
|
-
.action(async (options) => {
|
|
357
|
-
auth.requireAuth();
|
|
358
|
-
const ep = resolveEndpoint('mon.wazuh.deploy-history.list');
|
|
359
|
-
const data = await client.get(ep.path, { service: ep.service, query: { mode: options.mode, limit: options.limit } });
|
|
360
|
-
formatOutput({
|
|
361
|
-
// fleet #385 class sweep: the route returns
|
|
362
|
-
// { hostname, ip, ssh_user, success, message, deployed_at }, but this
|
|
363
|
-
// declared hostIp/status/deployedAt — three of four columns rendered
|
|
364
|
-
// as dashes with no error. `success` is a boolean, so it's mapped to
|
|
365
|
-
// a readable status rather than printed raw.
|
|
366
|
-
columns: ['hostname', 'ip', 'status', 'deployed_at'],
|
|
367
|
-
headers: ['Host', 'IP', 'Status', 'Deployed'],
|
|
368
|
-
rows: (data.deploys || []).map(d => ({
|
|
369
|
-
...d,
|
|
370
|
-
status: d.success ? 'ok' : (d.message ? `failed: ${String(d.message).slice(0, 40)}` : 'failed'),
|
|
371
|
-
})),
|
|
372
|
-
}, { format: program.opts().output, emptyMessage: 'No deploy history found.' });
|
|
373
|
-
});
|
|
277
|
+
// Read verbs (status, agents, alerts, vulns, deploy-history) are the top-level `quox wazuh` implementation: one code path.
|
|
278
|
+
// WAZ-10: the three legacy write verbs keep their flags but run through the SAME governed helpers as
|
|
279
|
+
// `quox wazuh deploy|deploy-bulk|active-response` (wazuh_* tools, held for a human approval). The old
|
|
280
|
+
// /api/v1/wazuh write routes are retired (410).
|
|
281
|
+
const { guard, writeTool, deployOne, WazFail } = attachWazuhVerbs(wazuh, program, { client, auth }, { writes: false });
|
|
282
|
+
const padId = (i) => (/^\d+$/.test(i) ? i.padStart(3, '0') : i);
|
|
283
|
+
const refuseTargetOverride = (verb, options) => {
|
|
284
|
+
if (options.wazuhServer || options.bastionHost) {
|
|
285
|
+
throw new WazFail(`quox mon wazuh ${verb}: --wazuh-server and --bastion-host are not accepted on the governed path, which always uses the org's configured Wazuh manager and bastion. Drop them, or change the Wazuh plugin configuration. Governed verb: quox wazuh ${verb === 'deploy-agent' ? 'deploy' : verb}`);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
374
288
|
|
|
375
289
|
wazuh
|
|
376
290
|
.command('deploy-agent')
|
|
377
|
-
.description('
|
|
291
|
+
.description('Install the Wazuh agent on one host (governed wazuh_deploy_agent, held for human approval; same as `quox wazuh deploy`)')
|
|
378
292
|
.requiredOption('--hostname <hostname>', 'target hostname')
|
|
379
293
|
.requiredOption('--host-ip <ip>', 'target host IP')
|
|
380
|
-
.option('--wazuh-server <ip>', '
|
|
381
|
-
.option('--bastion-host <ip>', '
|
|
294
|
+
.option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
|
|
295
|
+
.option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
|
|
382
296
|
.option('--ssh-user <user>', 'SSH user', 'control')
|
|
383
297
|
.option('--ssh-port <n>', 'SSH port', '22')
|
|
384
298
|
.option('--force', 'skip confirmation')
|
|
385
|
-
.action(async (options) => {
|
|
299
|
+
.action(guard(async (options) => {
|
|
386
300
|
auth.requireAuth();
|
|
387
|
-
|
|
301
|
+
refuseTargetOverride('deploy-agent', options);
|
|
302
|
+
const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${options.hostname} (${options.hostIp})? It is held for human approval before it runs.`);
|
|
388
303
|
if (!confirmed) {
|
|
389
304
|
printInfo('Cancelled.');
|
|
390
305
|
return;
|
|
391
306
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
printSuccess(`Deploy queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
|
|
398
|
-
});
|
|
307
|
+
await deployOne(options.hostname, options.hostIp, {
|
|
308
|
+
sshUser: options.sshUser && options.sshUser !== 'control' ? options.sshUser : undefined,
|
|
309
|
+
sshPort: options.sshPort && String(options.sshPort) !== '22' ? options.sshPort : undefined,
|
|
310
|
+
});
|
|
311
|
+
}));
|
|
399
312
|
|
|
400
313
|
wazuh
|
|
401
314
|
.command('deploy-bulk')
|
|
402
|
-
.description('
|
|
403
|
-
.requiredOption('--hosts <json>', 'JSON array of {hostname, ip} objects')
|
|
404
|
-
.option('--wazuh-server <ip>', '
|
|
405
|
-
.option('--bastion-host <ip>', '
|
|
315
|
+
.description('Install the Wazuh agent on several hosts: one governed wazuh_deploy_agent call and one approval per host (same as `quox wazuh deploy-bulk`)')
|
|
316
|
+
.requiredOption('--hosts <json>', 'JSON array of {hostname, ip[, sshUser, sshPort]} objects')
|
|
317
|
+
.option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
|
|
318
|
+
.option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
|
|
406
319
|
.option('--force', 'skip confirmation')
|
|
407
320
|
.action(async (options) => {
|
|
408
321
|
auth.requireAuth();
|
|
@@ -410,40 +323,49 @@ export function registerMonCommands(program, ctx) {
|
|
|
410
323
|
if (!Array.isArray(hosts) || hosts.length === 0) {
|
|
411
324
|
throw new Error('--hosts must be a non-empty JSON array of {hostname, ip} objects.');
|
|
412
325
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
326
|
+
await guard(async () => {
|
|
327
|
+
refuseTargetOverride('deploy-bulk', options);
|
|
328
|
+
const missing = hosts.filter((h) => !h || !h.hostname || !h.ip).map((h) => (h && h.hostname) || '?');
|
|
329
|
+
if (missing.length) throw new WazFail(`quox mon wazuh deploy-bulk: every host needs hostname and ip; missing for ${missing.join(', ')}`);
|
|
330
|
+
const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${hosts.length} host(s)? Each is held for human approval before it runs.`);
|
|
331
|
+
if (!confirmed) {
|
|
332
|
+
printInfo('Cancelled.');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let failed = 0;
|
|
336
|
+
for (const h of hosts) {
|
|
337
|
+
try { await deployOne(String(h.hostname), String(h.ip), { sshUser: h.sshUser, sshPort: h.sshPort }); } catch (err) {
|
|
338
|
+
if (!(err instanceof WazFail)) throw err;
|
|
339
|
+
console.error(err.message); failed++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (failed) process.exitCode = 1;
|
|
343
|
+
})();
|
|
424
344
|
});
|
|
425
345
|
|
|
426
346
|
wazuh
|
|
427
347
|
.command('active-response')
|
|
428
|
-
.description('
|
|
348
|
+
.description('Run a Wazuh active response (governed wazuh_active_response, held for human approval; same as `quox wazuh active-response`)')
|
|
429
349
|
.requiredOption('--command <command>', 'firewall-block|host-isolate|restart-wazuh-agent')
|
|
430
350
|
.requiredOption('--agents <ids>', 'comma-separated Wazuh agent IDs')
|
|
431
351
|
.requiredOption('--reason <text>', 'justification (min 10 characters)')
|
|
432
352
|
.option('--args <args>', 'comma-separated command arguments')
|
|
433
353
|
.option('--force', 'skip confirmation')
|
|
434
|
-
.action(async (options) => {
|
|
354
|
+
.action(guard(async (options) => {
|
|
435
355
|
auth.requireAuth();
|
|
436
|
-
const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean);
|
|
356
|
+
const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean).map(padId);
|
|
437
357
|
const args = options.args ? options.args.split(',').map(a => a.trim()).filter(Boolean) : [];
|
|
438
|
-
|
|
358
|
+
if (!agentList.length) throw new WazFail('quox mon wazuh active-response: --agents needs at least one agent id');
|
|
359
|
+
if (!options.reason || options.reason.trim().length < 10) throw new WazFail('quox mon wazuh active-response: --reason "<why>" is required, at least 10 characters (the approver sees it)');
|
|
360
|
+
const confirmed = await confirmOrSkip(program, options, `Request active response "${options.command}" on ${agentList.length} agent(s)? It is held for human approval before it runs.`);
|
|
439
361
|
if (!confirmed) {
|
|
440
362
|
printInfo('Cancelled.');
|
|
441
363
|
return;
|
|
442
364
|
}
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
});
|
|
365
|
+
const input = { command: options.command, agent_ids: agentList, reason: options.reason.trim() };
|
|
366
|
+
if (args.length) input.args = args;
|
|
367
|
+
await writeTool(`active-response ${agentList.join(',')}`, 'wazuh_active_response', input, `${options.command} ran on ${agentList.length} agent(s)`);
|
|
368
|
+
}));
|
|
447
369
|
|
|
448
370
|
// ══════════════════════════════════════════════════════════════════════
|
|
449
371
|
// agh-chain
|
|
@@ -461,6 +383,7 @@ export function registerMonCommands(program, ctx) {
|
|
|
461
383
|
auth.requireAuth();
|
|
462
384
|
const ep = resolveEndpoint('mon.agh-chain.leaderboard.list');
|
|
463
385
|
const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
|
|
386
|
+
if (reportComputing(program, data, 'The leaderboard is computing, try again shortly.')) return;
|
|
464
387
|
formatOutput({
|
|
465
388
|
columns: ['agent_id', 'claims_made', 'claims_verified', 'claims_stripped', 'unfulfilled_promises', 'honesty_rate'],
|
|
466
389
|
headers: ['Agent', 'Claims Made', 'Verified', 'Stripped', 'Unfulfilled', 'Honesty Rate'],
|
|
@@ -477,6 +400,7 @@ export function registerMonCommands(program, ctx) {
|
|
|
477
400
|
auth.requireAuth();
|
|
478
401
|
const ep = resolveEndpoint('mon.agh-chain.leaderboard.get', { agentId });
|
|
479
402
|
const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
|
|
403
|
+
if (reportComputing(program, data, 'These honesty metrics are computing, try again shortly.')) return;
|
|
480
404
|
formatDetail(data, { format: program.opts().output, title: `Honesty metrics: ${agentId}` });
|
|
481
405
|
});
|
|
482
406
|
|
|
@@ -490,6 +414,7 @@ export function registerMonCommands(program, ctx) {
|
|
|
490
414
|
auth.requireAuth();
|
|
491
415
|
const ep = resolveEndpoint('mon.agh-chain.leaderboard.events.list', { agentId });
|
|
492
416
|
const data = await client.get(ep.path, { service: ep.service, query: { window: options.window, guard: options.guard } });
|
|
417
|
+
if (reportComputing(program, data, 'These drill-through events are computing, try again shortly.')) return;
|
|
493
418
|
formatOutput({
|
|
494
419
|
columns: ['event_id', 'run_id', 'ts', 'payload_preview'],
|
|
495
420
|
headers: ['Event', 'Run', 'Time', 'Preview'],
|
|
@@ -10,10 +10,46 @@
|
|
|
10
10
|
* GET /api/v1/health/aggregate — same endpoint `quox status` uses
|
|
11
11
|
* GET /governance/incidents — list
|
|
12
12
|
* GET /governance/incidents/:id — detail + linked AEE envelopes
|
|
13
|
+
* POST /governance/incidents/:id/transition — claim (self only) / move status, witnessed
|
|
14
|
+
* (quox.governance.incident.updated; lib/governanceIncidentLifecycle.js)
|
|
15
|
+
*
|
|
16
|
+
* `incidents list --json` prints a bare array of incidents, the CLI-wide
|
|
17
|
+
* formatOutput convention for list verbs; proof cases read it by index
|
|
18
|
+
* (docs/matrix/suites/monitoring.json mon-incidents-open, mon-incident-detail),
|
|
19
|
+
* so the shape stays as is. `get --json` prints the incident object.
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
22
|
import { formatOutput, formatDetail } from '../core/output.js';
|
|
16
23
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
24
|
+
import { QuoxError } from '../core/errors.js';
|
|
25
|
+
|
|
26
|
+
// WAZ P11 slice 1b: statuses a person can move an incident to from the CLI. The collector
|
|
27
|
+
// validates against INCIDENT_STATUSES and enforces the resolution rule too; this only saves a call.
|
|
28
|
+
export const MOVE_STATUSES = ['investigating', 'resolved', 'closed', 'false_positive'];
|
|
29
|
+
const NEEDS_RESOLUTION = new Set(['resolved', 'closed', 'false_positive']);
|
|
30
|
+
const RESOLUTION_MAX = 500;
|
|
31
|
+
|
|
32
|
+
function printTransition(verb, id, data) {
|
|
33
|
+
const inc = data.incident || {};
|
|
34
|
+
const t = data.transition || {};
|
|
35
|
+
const w = data.witnessed || {};
|
|
36
|
+
const who = inc.assigned_to ? ` · assigned to ${inc.assigned_to}` : '';
|
|
37
|
+
const move = t.from && t.to ? `${t.from} -> ${t.to}` : (inc.status || '-');
|
|
38
|
+
const wit = w.stored ? `witnessed${w.id ? ` (${w.id})` : ''}` : `NOT witnessed (${w.reason || 'no envelope stored'})`;
|
|
39
|
+
console.log(`${w.stored ? '✔' : '⚠'} incidents ${verb} │ ${inc.id || id} · ${move}${who} │ ${wit}`);
|
|
40
|
+
if (inc.resolution && NEEDS_RESOLUTION.has(inc.status)) console.log(` resolution: ${inc.resolution}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Module level (not a closure) so cli/scripts/export-leaf-methods.mjs sees the POST as evidence for both leaves.
|
|
44
|
+
async function postTransition(client, program, verb, id, body) {
|
|
45
|
+
const ep = resolveEndpoint('governance.incidents.transition', { id });
|
|
46
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
47
|
+
if (!data || data.error || !data.incident) {
|
|
48
|
+
throw new QuoxError(`quox monitoring incidents ${verb}: ${ep.method} ${ep.path} returned an error body: ${(data && (data.error || data.message)) || 'response had no incident'}`, 1);
|
|
49
|
+
}
|
|
50
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
51
|
+
printTransition(verb, id, data);
|
|
52
|
+
}
|
|
17
53
|
|
|
18
54
|
export function registerMonitoringCommands(program, ctx) {
|
|
19
55
|
const { client, auth } = ctx;
|
|
@@ -83,4 +119,36 @@ export function registerMonitoringCommands(program, ctx) {
|
|
|
83
119
|
const data = await client.get(resolveEndpoint('governance.incidents.get', { id }).path, { service: resolveEndpoint('governance.incidents.get', { id }).service });
|
|
84
120
|
formatDetail(data.incident || data, { format: program.opts().output, title: `Incident: ${id}` });
|
|
85
121
|
});
|
|
122
|
+
|
|
123
|
+
incidents
|
|
124
|
+
.command('claim')
|
|
125
|
+
.description('Claim a governance incident: assigns it to you and moves open to investigating (witnessed)')
|
|
126
|
+
.argument('<id>', 'incident ID from `quox monitoring incidents list`')
|
|
127
|
+
.action(async (id) => {
|
|
128
|
+
auth.requireAuth();
|
|
129
|
+
await postTransition(client, program, 'claim', id, { assigned_to: 'me' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
incidents
|
|
133
|
+
.command('status')
|
|
134
|
+
.description(`Move a governance incident to ${MOVE_STATUSES.join('|')}; resolved, closed and false_positive need --resolution (witnessed)`)
|
|
135
|
+
.argument('<id>', 'incident ID from `quox monitoring incidents list`')
|
|
136
|
+
.argument('<status>', MOVE_STATUSES.join('|'))
|
|
137
|
+
.option('--resolution <text>', `what was done or why it is closing (required for resolved, closed, false_positive; max ${RESOLUTION_MAX} chars)`)
|
|
138
|
+
.action(async (id, status, options) => {
|
|
139
|
+
if (!MOVE_STATUSES.includes(status)) {
|
|
140
|
+
throw new QuoxError(`quox monitoring incidents status: status must be one of: ${MOVE_STATUSES.join(', ')} (got "${status}").`, 1);
|
|
141
|
+
}
|
|
142
|
+
const resolution = typeof options.resolution === 'string' ? options.resolution.trim() : '';
|
|
143
|
+
if (NEEDS_RESOLUTION.has(status) && !resolution) {
|
|
144
|
+
throw new QuoxError(`quox monitoring incidents status: moving to ${status} needs --resolution "<what was done>".`, 1);
|
|
145
|
+
}
|
|
146
|
+
if (resolution.length > RESOLUTION_MAX) {
|
|
147
|
+
throw new QuoxError(`quox monitoring incidents status: --resolution is at most ${RESOLUTION_MAX} characters (got ${resolution.length}).`, 1);
|
|
148
|
+
}
|
|
149
|
+
auth.requireAuth();
|
|
150
|
+
const body = { status };
|
|
151
|
+
if (resolution) body.resolution = resolution;
|
|
152
|
+
await postTransition(client, program, 'status', id, body);
|
|
153
|
+
});
|
|
86
154
|
}
|
package/lib/commands/notes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { spawn } from 'child_process';
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { basename, dirname, join, resolve } from 'path';
|
|
12
12
|
import { Option } from 'commander';
|
|
13
|
+
import { resolveProfileFlagFromArgv } from '../core/config.js';
|
|
13
14
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
14
15
|
import { QuoxApiError, QuoxError } from '../core/errors.js';
|
|
15
16
|
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
@@ -18,6 +19,7 @@ import { resolveOrgId } from './helpers.js';
|
|
|
18
19
|
import { buildBatchArchive, splitMarkdown, indexMarkdown, MAX_BATCH_NOTES } from './notesBatch.js';
|
|
19
20
|
import { emailToNote, extractTasks, promoteBody } from './notesBridges.js';
|
|
20
21
|
import { linkMention } from './notesLinks.js';
|
|
22
|
+
import { registerNotesSeal } from './notesSeal.js';
|
|
21
23
|
|
|
22
24
|
const SCOPES = ['org', 'user'];
|
|
23
25
|
const ARCHIVED = ['exclude', 'only', 'include'];
|
|
@@ -156,6 +158,8 @@ export function registerNotesCommands(program, ctx) {
|
|
|
156
158
|
.command('notes')
|
|
157
159
|
.description('NotesQ: governed markdown notes (list, read, write, history, restore)');
|
|
158
160
|
|
|
161
|
+
registerNotesSeal(notes, { program, client, auth, call, getNote, putNote, readContent, isJson, printJson, announce, loadEnqrypt: ctx.loadEnqrypt });
|
|
162
|
+
|
|
159
163
|
const list = async (options, textQuery) => {
|
|
160
164
|
auth.requireAuth();
|
|
161
165
|
checkEnum(options.scope, [...SCOPES, 'all'], '--scope');
|
|
@@ -188,10 +192,18 @@ export function registerNotesCommands(program, ctx) {
|
|
|
188
192
|
updated: when(n),
|
|
189
193
|
}));
|
|
190
194
|
formatOutput({
|
|
191
|
-
columns: ['
|
|
192
|
-
headers: ['
|
|
195
|
+
columns: ['title', 'scope', 'collection', 'tags', 'updated', ...propKeys.map((_, i) => `prop${i}`), 'id'],
|
|
196
|
+
headers: ['Title', 'Scope', 'Collection', 'Tags', 'Updated', ...propKeys, 'ID'],
|
|
193
197
|
rows,
|
|
194
|
-
}, {
|
|
198
|
+
}, {
|
|
199
|
+
format: program.opts().output,
|
|
200
|
+
quietColumn: 'id',
|
|
201
|
+
emptyMessage: 'No notes found. Widen the filters (--scope all, drop --tag/--q), or create one with `quox notes new`.',
|
|
202
|
+
});
|
|
203
|
+
const total = data.total ?? rows.length;
|
|
204
|
+
if (program.opts().output !== 'json' && rows.length > 0 && rows.length < total) {
|
|
205
|
+
console.log(`showing ${rows.length} of ${total} · --limit to see more`);
|
|
206
|
+
}
|
|
195
207
|
announce('notes ls', `${rows.length} of ${data.total ?? rows.length}`);
|
|
196
208
|
};
|
|
197
209
|
|
|
@@ -1116,8 +1128,15 @@ export function registerNotesCommands(program, ctx) {
|
|
|
1116
1128
|
auth.requireAuth();
|
|
1117
1129
|
const ep = resolveEndpoint('notesq.notes.proposeSkill', { id });
|
|
1118
1130
|
const body = { ...(options.name ? { name: options.name } : {}), ...(options.category ? { category: options.category } : {}) };
|
|
1131
|
+
// The route refuses a note under its minimum length with 400 {error:'too_short', min_words}: say so, not a bare 400.
|
|
1132
|
+
const propose = (b, opts = {}) => call(() => client.post(ep.path, { service: ep.service, body: b, ...opts })).catch((e) => {
|
|
1133
|
+
if (e instanceof QuoxApiError && e.status === 400 && e.body && e.body.error === 'too_short') {
|
|
1134
|
+
throw new QuoxError(`Note ${id} is too short to become a skill: it needs at least ${e.body.min_words || 'more'} words of procedure. Add the steps (quox notes edit ${id}) and run again.`, 1);
|
|
1135
|
+
}
|
|
1136
|
+
throw e;
|
|
1137
|
+
});
|
|
1119
1138
|
if (!options.yes) {
|
|
1120
|
-
const r = await
|
|
1139
|
+
const r = await propose({ ...body, dry_run: true });
|
|
1121
1140
|
if (isJson(program)) return printJson(r);
|
|
1122
1141
|
const p = r.payload || {};
|
|
1123
1142
|
console.log(`name: ${p.name}`);
|
|
@@ -1126,7 +1145,7 @@ export function registerNotesCommands(program, ctx) {
|
|
|
1126
1145
|
console.log(String(p.bodyMd || '').split('\n').slice(0, 8).join('\n'));
|
|
1127
1146
|
return console.log('Re-run with --yes to propose it. It lands as an experimental skill, outside the stable library; an automated safety check reviews it and anything flagged goes to the human review queue (quox skills review).');
|
|
1128
1147
|
}
|
|
1129
|
-
const r = await
|
|
1148
|
+
const r = await propose(body, { retries: 0 });
|
|
1130
1149
|
if (isJson(program)) return printJson(r);
|
|
1131
1150
|
announce('propose-skill', `${r.skill.name} (${r.skill.id}) experimental`);
|
|
1132
1151
|
if (!r.note_updated) console.log('Note: the skill was created but the note could not be updated with the link (it changed meanwhile).');
|
|
@@ -1225,7 +1244,7 @@ export function registerNotesCommands(program, ctx) {
|
|
|
1225
1244
|
|
|
1226
1245
|
notes
|
|
1227
1246
|
.command('scorecard')
|
|
1228
|
-
.description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output')
|
|
1247
|
+
.description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output. WRITE: creates notes through chat (raising approval cards) and deletes them afterwards; the child runs as the same -p profile')
|
|
1229
1248
|
.option('--keep', 'keep the scorecard\'s test data')
|
|
1230
1249
|
.action(async (options) => {
|
|
1231
1250
|
if (!existsSync(scorecardPath)) {
|
|
@@ -1235,7 +1254,10 @@ export function registerNotesCommands(program, ctx) {
|
|
|
1235
1254
|
const args = [scorecardPath];
|
|
1236
1255
|
if (options.keep) args.push('--keep');
|
|
1237
1256
|
const code = await new Promise((done, fail) => {
|
|
1238
|
-
|
|
1257
|
+
// The child must act as the profile the caller chose, never the resident/attached agent identity.
|
|
1258
|
+
const profile = ctx.profile || resolveProfileFlagFromArgv(process.argv.slice(2)) || process.env.QUOX_PROFILE;
|
|
1259
|
+
const env = profile ? { ...process.env, QUOX_PROFILE: profile } : process.env;
|
|
1260
|
+
const child = spawn(process.execPath, args, { cwd: resolve(dirname(scorecardPath), '..'), env, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
1239
1261
|
child.stdout.on('data', (c) => process.stdout.write(c));
|
|
1240
1262
|
child.stderr.on('data', (c) => process.stderr.write(c));
|
|
1241
1263
|
child.on('error', fail);
|