quoxcli 0.3.1 → 0.5.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.
Files changed (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * quox wazuh hooks ... / quox wazuh incidents ... : Wazuh alerts become witnessed Quox incidents (WAZ P6).
3
+ * The sibling of `quox kuma hooks` / `quox kuma incidents` (kuma.js).
4
+ *
5
+ * quox wazuh hooks list this org's alert hooks (never shows a secret)
6
+ * quox wazuh hooks create [--label <text>] new hook; prints the URL path, the secret ONCE and the ossec.conf block
7
+ * quox wazuh hooks rotate <hookId> new secret for a hook, shown ONCE; the old one stops working
8
+ * quox wazuh hooks delete <hookId> Wazuh posts to it are refused from then on
9
+ * quox wazuh hooks install --collector-url <url> [--level 7] [--label <l>]
10
+ * one governed step (held for approval): hook + custom-quox + ossec.conf + restart;
11
+ * the secret goes to the manager on Bastion stdin and is never shown
12
+ * quox wazuh incidents [--open] [--since <24h|ISO>] [--limit <n>]
13
+ * quox wazuh incidents show <id> context (Kuma maintenance, open Kuma incidents, deploys), diagnosis, escalation
14
+ * quox wazuh incidents close <id> --reason <text>
15
+ * quox wazuh incidents diagnose <id> read-only host reads; off-list reads are held for approval (WAZ P7)
16
+ * quox wazuh incidents escalate <id> [--engagement <id|name>] file it as a QuoxSecurity finding (WAZ P7)
17
+ *
18
+ * Hook management needs an org admin or owner (a signed-in person). Incidents: any org member.
19
+ * Backend: /api/v1/wazuh/hooks, /api/v1/wazuh/incidents (services/collector/lib/wazuhAlerts.js).
20
+ * Install on the Wazuh manager: docs/wazuh/custom-quox-integration.md.
21
+ */
22
+
23
+ import { resolveEndpoint } from '../api/endpoints.js';
24
+ import { relDuration } from './kuma.js';
25
+
26
+ const RUNBOOK = 'docs/wazuh/custom-quox-integration.md';
27
+
28
+ // WAZ P7: compact context flags. A source that could not answer shows as SRC?, never as "nothing".
29
+ const UNKNOWN_FLAG = { maintenance: 'MAINT?', kuma: 'KUMA?', deploys: 'DEPLOY?' };
30
+ export function contextFlags(ctx) {
31
+ if (!ctx || typeof ctx !== 'object') return '-';
32
+ const parts = [...(ctx.flags || []), ...(ctx.unavailable || []).map((s) => UNKNOWN_FLAG[s] || `${String(s).toUpperCase()}?`)];
33
+ return parts.length ? parts.join(' ') : '-';
34
+ }
35
+
36
+ const yesNo = (v) => (v === true ? 'yes' : v === false ? 'no' : 'unknown');
37
+ function contextRows(c) {
38
+ if (!c || typeof c !== 'object') return [];
39
+ const m = c.maintenance || {};
40
+ const k = c.kuma || {};
41
+ const d = c.deploys || {};
42
+ const unav = (x) => `unavailable: ${x.reason || 'no answer'}`;
43
+ const maint = m.status !== 'ok' ? unav(m)
44
+ : (m.now ? `in a Kuma maintenance window now (${(m.monitors || []).join(', ') || 'monitor'}); at the last alert: ${yesNo(m.at_last_seen)}`
45
+ : `not in a Kuma maintenance window now; at the last alert: ${yesNo(m.at_last_seen)}`);
46
+ const open = k.open || [];
47
+ const kuma = k.status !== 'ok' ? unav(k)
48
+ : (open.length ? `${open.length} open Kuma incident${open.length === 1 ? '' : 's'}: ${open.slice(0, 3).map((x) => `${x.monitor_name} (${x.id})`).join(', ')}` : 'no open Kuma incident for this host');
49
+ const recent = d.recent || [];
50
+ const dep = d.status !== 'ok' ? unav(d)
51
+ : (recent.length ? `${recent.length} Wazuh deploy${recent.length === 1 ? '' : 's'} to this host in the 24h before the last alert (latest ${recent[0].at}${recent[0].success === false ? ', failed' : ''})` : 'no Wazuh deploy to this host in the 24h before the last alert');
52
+ return [['Maintenance', maint], ['Kuma', kuma], ['Deploys', dep]];
53
+ }
54
+
55
+ function printChecks(d, pad) {
56
+ const rows = [['CHECK', 'STATUS', 'OK', 'DETAIL']];
57
+ for (const c of d.checks || []) rows.push([c.name, c.status, c.ok === true ? 'yes' : c.ok === false ? 'NO' : '-', `${c.detail || '-'}${c.approval_id ? ` (approval ${c.approval_id})` : ''}`]);
58
+ if (rows.length > 1) { console.log(''); console.log(pad(rows).join('\n')); }
59
+ for (const c of d.checks || []) for (const l of c.lines || []) console.log(` ${c.name}: ${l}`);
60
+ }
61
+
62
+ function ossecBlock(path, secret, level = 7) {
63
+ return [
64
+ '<integration>',
65
+ ' <name>custom-quox</name>',
66
+ ` <hook_url>COLLECTOR_BASE_URL${path}</hook_url>`,
67
+ ` <api_key>${secret}</api_key>`,
68
+ ` <level>${level}</level>`,
69
+ ' <alert_format>json</alert_format>',
70
+ '</integration>',
71
+ ].join('\n');
72
+ }
73
+
74
+ export function registerWazuhIntake(wazuh, { client, auth, jsonOut, guard, WazFail, pad, head, failFrom, example, callTool, failIfToolFailed }) {
75
+ const call = async (verb, key, params, fn, id, extra = {}) => {
76
+ const ep = resolveEndpoint(key, params);
77
+ try { return { ep, data: await fn(ep) }; } catch (err) {
78
+ const status = err && (err.status ?? err.statusCode);
79
+ const code = err && err.body && err.body.error_code;
80
+ const base = `quox wazuh ${verb}: ${ep.method} ${ep.path} failed${status ? ` with HTTP ${status}` : ''}`;
81
+ if (status === 403 && (code === 'admin_required' || /admin role required/i.test((err && err.message) || ''))) throw new WazFail(`${base}: managing Wazuh alert hooks needs an org admin or owner.`);
82
+ if (status === 403 && code === 'human_required') throw new WazFail(`${base}: this needs a signed-in person, not a service key.`);
83
+ if (status === 404 && code === 'engagement_not_found') throw new WazFail(`${base}: no engagement ${extra.engagement || ''} in this org. List them with \`quox engagement list\`, or leave out --engagement to create one.`);
84
+ if (status === 409 && code === 'already_escalated') {
85
+ const e = (err.body && err.body.escalation) || {};
86
+ throw new WazFail(`${base}: incident${id ? ` ${id}` : ''} is already escalated${e.finding_id ? ` as finding ${e.finding_id}` : ''}${e.engagement_id ? ` in engagement ${e.engagement_id}` : ''}.`);
87
+ }
88
+ if (status === 503 && code === 'store_unavailable') throw new WazFail(`${base}: the QuoxSecurity engagement store is not available on this collector.`);
89
+ if (status === 404 && /^hooks/.test(verb)) throw new WazFail(`${base}: no such hook${id ? ` (${id})` : ''} in this org. List them with \`quox wazuh hooks list\`.`);
90
+ if (status === 404) throw new WazFail(`${base}: no such incident${id ? ` (${id})` : ''} in this org. List them with \`quox wazuh incidents\`.`);
91
+ if (status === 409 && code === 'already_closed') throw new WazFail(`${base}: incident${id ? ` ${id}` : ''} is already closed.`);
92
+ throw failFrom(verb, ep.method, ep.path, err);
93
+ }
94
+ };
95
+ const errBody = (verb, ep, data, what) => new WazFail(`quox wazuh ${verb}: ${ep.method} ${ep.path} returned an error body: ${(data && (data.error || data.message)) || what}`);
96
+ const bad = (data) => !data || data.success === false || data.error;
97
+
98
+ // ---- hooks ----
99
+ const hooks = wazuh.command('hooks')
100
+ .description('The per-org signed hook Wazuh integratord posts alerts to (list, create, rotate, delete; org admin)');
101
+
102
+ example(hooks.command('list')
103
+ .description('This org\'s Wazuh alert hooks (never shows secrets)')
104
+ .action(guard(async () => {
105
+ auth.requireAuth();
106
+ const { ep, data } = await call('hooks list', 'mon.wazuh.hooks.list', {}, (e) => client.get(e.path, { service: e.service }));
107
+ if (bad(data) || !Array.isArray(data.hooks)) throw errBody('hooks list', ep, data, 'response had no hooks array');
108
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
109
+ head('hooks list', `${data.hooks.length} ${data.hooks.length === 1 ? 'hook' : 'hooks'}`);
110
+ if (!data.hooks.length) { console.log('No hooks yet. Create one: quox wazuh hooks create'); return; }
111
+ const rows = [['HOOK', 'LABEL', 'CREATED', 'ROTATED', 'LAST USED', 'PATH']];
112
+ for (const h of data.hooks) rows.push([h.hookId, h.label || '-', h.created_at || '-', h.rotated_at || '-', h.last_used_at || 'never', h.path || '-']);
113
+ console.log(pad(rows).join('\n'));
114
+ })), 'quox wazuh hooks list');
115
+
116
+ const printSecret = (data, verb) => {
117
+ console.log([
118
+ `URL path : ${data.path} (append to your Quox collector base URL)`,
119
+ `Secret : ${data.secret}`,
120
+ '',
121
+ 'The secret is shown ONCE and is stored only as a hash. Copy it now.',
122
+ '',
123
+ `Set it up on the Wazuh manager (full steps and the integration script: ${RUNBOOK}):`,
124
+ ' 1. Install /var/ossec/integrations/custom-quox (root:wazuh, 750) from the runbook.',
125
+ ' 2. Add this block inside <ossec_config> in /var/ossec/etc/ossec.conf:',
126
+ '',
127
+ ossecBlock(data.path, data.secret).split('\n').map((l) => ` ${l}`).join('\n'),
128
+ '',
129
+ ' 3. Restart the manager (systemctl restart wazuh-manager). Alerts at the chosen level and above',
130
+ ` open incidents: quox wazuh incidents --open. Level 12+ also alert in the inbox; ${verb === 'rotate' ? 'the old secret stops working now, update <api_key>.' : 'the rest go in the daily digest.'}`,
131
+ ].join('\n'));
132
+ };
133
+
134
+ example(hooks.command('create')
135
+ .description('Create a Wazuh alert hook; the secret is shown ONCE (org admin)')
136
+ .option('--label <text>', 'a name for this hook (for example the manager name)')
137
+ .action(guard(async (options) => {
138
+ auth.requireAuth();
139
+ const { ep, data } = await call('hooks create', 'mon.wazuh.hooks.create', {}, (e) => client.post(e.path, { service: e.service, body: options.label ? { label: options.label } : {} }));
140
+ if (bad(data) || !data.hookId || !data.secret) throw errBody('hooks create', ep, data, 'response had no hook');
141
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
142
+ head('hooks create', data.hookId);
143
+ printSecret(data, 'create');
144
+ })), 'quox wazuh hooks create --label wz-mgr-01');
145
+
146
+ example(hooks.command('rotate')
147
+ .description('Issue a new secret for a hook, shown ONCE; the old secret stops working at once (org admin)')
148
+ .argument('<hookId>', 'hook id from `quox wazuh hooks list`')
149
+ .action(guard(async (hookId) => {
150
+ auth.requireAuth();
151
+ const { ep, data } = await call('hooks rotate', 'mon.wazuh.hooks.rotate', { id: hookId }, (e) => client.post(e.path, { service: e.service, body: {} }), hookId);
152
+ if (bad(data) || !data.secret) throw errBody('hooks rotate', ep, data, 'response had no secret');
153
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
154
+ head('hooks rotate', `${data.hookId || hookId} · old secret stops working now`, 'warn');
155
+ printSecret({ ...data, path: data.path || ep.path.replace(/\/rotate$/, '') }, 'rotate');
156
+ })), 'quox wazuh hooks rotate wh_0123456789abcdef');
157
+
158
+ example(hooks.command('delete')
159
+ .description('Delete a Wazuh alert hook; posts to it are refused from then on (org admin)')
160
+ .argument('<hookId>', 'hook id from `quox wazuh hooks list`')
161
+ .action(guard(async (hookId) => {
162
+ auth.requireAuth();
163
+ const { ep, data } = await call('hooks delete', 'mon.wazuh.hooks.delete', { id: hookId }, (e) => client.delete(e.path, { service: e.service }), hookId);
164
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
165
+ if (bad(data)) throw errBody('hooks delete', ep, data, 'no body');
166
+ head('hooks delete', `deleted ${hookId}`);
167
+ })), 'quox wazuh hooks delete wh_0123456789abcdef');
168
+
169
+ if (callTool) {
170
+ // WAZ-6: the governed one-step install. Validated here so a typo never files an approval; the collector re-validates.
171
+ example(hooks.command('install')
172
+ .description('Wire the Wazuh manager to Quox alert intake: hook, custom-quox integration, ossec.conf (backed up), restart (held for human approval; the secret is never shown)')
173
+ .requiredOption('--collector-url <url>', 'base URL the Wazuh manager reaches Quox on, http(s)://host[:port], no path')
174
+ .option('--level <n>', 'lowest Wazuh rule level sent (3-15)', '7')
175
+ .option('--allow-http', 'allow plain http to a private LAN IP (the hook secret then crosses the LAN unencrypted; shown on the approval card)')
176
+ .option('--label <label>', 'hook label; an existing hook with this label is rotated, not duplicated (default wazuh-<host>)')
177
+ .action(guard(async (options) => {
178
+ const verb = 'hooks install';
179
+ const raw = String(options.collectorUrl || '').trim();
180
+ let u = null;
181
+ try { u = new URL(raw); } catch { u = null; }
182
+ if (!u || !/^https?:$/.test(u.protocol) || u.username || u.password || raw.includes('@') || /[?#\s]/.test(raw) || (u.pathname !== '/' && u.pathname !== '')) {
183
+ throw new WazFail(`quox wazuh ${verb}: --collector-url must be an http(s) base URL with no credentials, path or query, for example https://quox.example:9848`);
184
+ }
185
+ const lv = String(options.level).trim();
186
+ if (!/^\d{1,2}$/.test(lv) || Number(lv) < 3 || Number(lv) > 15) throw new WazFail(`quox wazuh ${verb}: --level must be a whole number from 3 to 15`);
187
+ const input = { collector_url: raw.replace(/\/$/, ''), level: Number(lv) };
188
+ if (options.allowHttp) input.allow_http = true;
189
+ if (options.label) input.label = String(options.label);
190
+ auth.requireAuth();
191
+ const { data, pendingId } = await callTool(verb, 'wazuh_hooks_install', input);
192
+ if (jsonOut()) {
193
+ console.log(JSON.stringify(data, null, 2));
194
+ if (!pendingId) { try { failIfToolFailed(verb, 'wazuh_hooks_install', data); } catch { process.exitCode = 1; } }
195
+ return;
196
+ }
197
+ if (pendingId) { head(verb, 'awaiting approval', 'busy'); console.log(`◐ awaiting approval ${pendingId}: a person approves it in the inbox; check with quox approvals result ${pendingId}`); return; }
198
+ const r = failIfToolFailed(verb, 'wazuh_hooks_install', data);
199
+ head(verb, `${r.hook_id} on ${r.host}`, 'ok');
200
+ console.log(pad([
201
+ ['Hook', `${r.hook_id}${r.rotated ? ' (rotated: new secret)' : ' (new)'}`],
202
+ ['Host', r.host],
203
+ ['Level', `${r.level} and above`],
204
+ ['Sends to', r.hook_url || '-'],
205
+ ['Backup', r.backup || '-'],
206
+ ['Manager', 'wazuh-manager restart requested'],
207
+ ]).join('\n'));
208
+ console.log('Check it: quox wazuh hooks list (LAST USED fills in at the first alert), quox wazuh incidents --open');
209
+ })), 'quox wazuh hooks install --collector-url https://quox.example:9848 --level 7 --label wz-mgr-01');
210
+ }
211
+
212
+ // ---- incidents ----
213
+ const ago = (iso) => { const t = Date.parse(iso); return Number.isFinite(t) ? relDuration(Date.now() - t).replace(/ /g, '') : (iso || '-'); };
214
+
215
+ const incidents = example(wazuh.command('incidents')
216
+ .description('Wazuh alerts that became Quox incidents (one per rule and agent; repeats counted, not re-alerted)')
217
+ .option('--open', 'only incidents not yet closed')
218
+ .option('--since <when>', 'last seen since a duration (30m, 24h, 7d) or an ISO time')
219
+ .option('--limit <n>', 'max rows (default 50, max 200)')
220
+ .action(guard(async (options) => {
221
+ auth.requireAuth();
222
+ const { ep, data } = await call('incidents', 'mon.wazuh.incidents.list', { open: !!options.open, since: options.since, limit: options.limit }, (e) => client.get(e.path, { service: e.service }));
223
+ if (bad(data) || !Array.isArray(data.incidents)) throw errBody('incidents', ep, data, 'response had no incidents array');
224
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
225
+ const openN = data.incidents.filter((i) => i.open).length;
226
+ head('incidents', `${data.incidents.length} shown · ${openN} open`, openN ? 'warn' : 'ok');
227
+ if (!data.incidents.length) return;
228
+ // WAZ P7: CONTEXT (MAINT / KUMA-DOWN / DEPLOYED, SRC? when a source could not answer) when the collector sent it.
229
+ const withCtx = data.incidents.some((i) => i.context);
230
+ const rows = [['ID', 'STATE', 'LEVEL', 'AGENT', 'RULE', 'DESCRIPTION', 'COUNT', 'LAST SEEN', 'MITRE', ...(withCtx ? ['CONTEXT'] : [])]];
231
+ for (const i of data.incidents) {
232
+ rows.push([i.id, i.open ? 'open' : `closed${i.close_reason === 'quiet' ? ' (quiet)' : ''}`, i.level, i.agent_name || i.agent_id, i.rule_id,
233
+ String(i.rule_description || '-').slice(0, 60), i.count ?? '-', ago(i.last_seen), (i.mitre_ids || []).join(',') || '-', ...(withCtx ? [contextFlags(i.context)] : [])]);
234
+ }
235
+ console.log(pad(rows).join('\n'));
236
+ })), 'quox wazuh incidents --open --since 24h');
237
+
238
+ example(incidents.command('show')
239
+ .description('One Wazuh incident, including its stored (truncated) log line')
240
+ .argument('<id>', 'incident id from `quox wazuh incidents`')
241
+ .action(guard(async (id) => {
242
+ auth.requireAuth();
243
+ const { ep, data } = await call('incidents show', 'mon.wazuh.incidents.get', { id }, (e) => client.get(e.path, { service: e.service }), id);
244
+ if (bad(data) || !data.incident) throw errBody('incidents show', ep, data, 'response had no incident');
245
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
246
+ const i = data.incident;
247
+ head('incidents show', `${i.id} · ${i.open ? 'open' : 'closed'}`, i.open ? 'warn' : 'ok');
248
+ const rows = [
249
+ ['Rule', `${i.rule_id} (level ${i.level}) ${i.rule_description || ''}`],
250
+ ['Agent', `${i.agent_name || '-'} (agent ${i.agent_id}${i.agent_ip ? `, ${i.agent_ip}` : ''})`],
251
+ ['Groups', (i.rule_groups || []).join(', ') || '-'],
252
+ ['MITRE', [...(i.mitre_ids || []), ...(i.mitre_techniques || []), ...(i.mitre_tactics || [])].join(', ') || '-'],
253
+ ['Location', i.location || '-'],
254
+ ['Seen', `${i.count} time(s), first ${i.first_seen}, last ${i.last_seen}`],
255
+ ['Alerted', i.notified ? 'yes (inbox)' : (i.suppressed && i.suppressed.reason === 'maintenance'
256
+ ? `no: suppressed, host was in a Kuma maintenance window (${(i.suppressed.monitors || []).join(', ') || 'monitor'})`
257
+ : 'no (digest only)')],
258
+ ['Log', i.log_snippet || '-'],
259
+ ...contextRows(i.context),
260
+ ];
261
+ const dg = i.diagnosis;
262
+ rows.push(['Diagnosis', dg ? `${dg.status} (${dg.trigger || dg.mode || 'manual'}) ${dg.summary || ''}`.trim() : `not run (quox wazuh incidents diagnose ${i.id})`]);
263
+ const es = i.escalation;
264
+ rows.push(['Escalated', es && es.finding_id ? `finding ${es.finding_id} in engagement ${es.engagement_id}${es.engagement_name ? ` (${es.engagement_name})` : ''}` : `no (quox wazuh incidents escalate ${i.id})`]);
265
+ if (!i.open) rows.push(['Closed', `${i.closed_at}${i.closed_by ? ` by ${i.closed_by}` : ''}${i.close_reason ? `: ${i.close_reason}` : ''}`]);
266
+ console.log(pad(rows).join('\n'));
267
+ if (dg) printChecks(dg, pad);
268
+ })), 'quox wazuh incidents show WINC_abc');
269
+
270
+ example(incidents.command('close')
271
+ .description('Close a Wazuh incident with what you found (witnessed)')
272
+ .argument('<id>', 'incident id from `quox wazuh incidents`')
273
+ .requiredOption('--reason <text>', 'what you found, for the record')
274
+ .action(guard(async (id, options) => {
275
+ auth.requireAuth();
276
+ const { ep, data } = await call('incidents close', 'mon.wazuh.incidents.close', { id }, (e) => client.post(e.path, { service: e.service, body: { reason: String(options.reason) } }), id);
277
+ if (bad(data) || !data.incident) throw errBody('incidents close', ep, data, 'response had no incident');
278
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
279
+ head('incidents close', `closed ${data.incident.id || id}`);
280
+ })), 'quox wazuh incidents close WINC_abc --reason "known scanner, blocked at edge-02"');
281
+
282
+ // ---- WAZ P7: diagnose + escalate ----
283
+ example(incidents.command('diagnose')
284
+ .description('Read-only host diagnosis for an incident (auth failures, sessions, ports, processes, logins, the unit): allowlisted reads run, the rest are held for a human approval')
285
+ .argument('<id>', 'incident id from `quox wazuh incidents`')
286
+ .action(guard(async (id) => {
287
+ auth.requireAuth();
288
+ const { ep, data } = await call('incidents diagnose', 'mon.wazuh.incidents.diagnose', { id }, (e) => client.post(e.path, { service: e.service, body: {} }), id);
289
+ if (bad(data) || !data.incident || !data.incident.diagnosis) throw errBody('incidents diagnose', ep, data, 'response had no diagnosis');
290
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
291
+ const d = data.incident.diagnosis;
292
+ head('incidents diagnose', `${id} · ${d.status}${d.host ? ` on ${d.host}` : ''}`, d.status === 'ran' ? 'ok' : (d.status === 'refused' || d.status === 'unavailable' ? 'fail' : 'warn'));
293
+ console.log(d.summary || '-');
294
+ printChecks(d, pad);
295
+ const held = (d.checks || []).filter((c) => c.approval_id);
296
+ if (held.length) {
297
+ console.log('');
298
+ for (const c of held) console.log(`◐ ${c.name} is held for approval ${c.approval_id}: a person approves it in the inbox; read the output with quox approvals result ${c.approval_id}`);
299
+ }
300
+ })), 'quox wazuh incidents diagnose WINC_abc');
301
+
302
+ example(incidents.command('escalate')
303
+ .description('File the incident as a finding in a QuoxSecurity engagement (a new wazuh-incident-<id> engagement unless --engagement is given); the finding id is linked on the incident')
304
+ .argument('<id>', 'incident id from `quox wazuh incidents`')
305
+ .option('--engagement <idOrName>', 'an existing engagement of this org (id or exact name)')
306
+ .action(guard(async (id, options) => {
307
+ auth.requireAuth();
308
+ const body = options.engagement ? { engagement: String(options.engagement) } : {};
309
+ const { ep, data } = await call('incidents escalate', 'mon.wazuh.incidents.escalate', { id }, (e) => client.post(e.path, { service: e.service, body }), id, { engagement: options.engagement });
310
+ if (bad(data) || !data.finding || !data.engagement) throw errBody('incidents escalate', ep, data, 'response had no finding');
311
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
312
+ const f = data.finding;
313
+ head('incidents escalate', `finding ${f.id} filed`);
314
+ console.log(`Filed finding ${f.id} (${f.severity}, ${f.status}) in ${data.created_engagement ? 'new ' : ''}engagement ${data.engagement.id}.`);
315
+ console.log(`Engagement: ${data.engagement.name || data.engagement.id}. Next: quox engagement findings ${data.engagement.id}; verify it: quox engagement verify-finding ${f.id} confirmed|refuted --reason "..."`);
316
+ })), 'quox wazuh incidents escalate WINC_abc --engagement soc-week-45');
317
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * quox wazuh report ... : signed, offline-verifiable Wazuh security posture reports (WAZ P5).
3
+ * The sibling of `quox kuma sla` (kumaSla.js): same canonical form, same signer, same bundle shape.
4
+ *
5
+ * quox wazuh report generate [--agent <id|name>] [--out <file>] build + sign + store a posture report
6
+ * quox wazuh report list this org's stored reports
7
+ * quox wazuh report show <id> [--out <file>] one stored report (and its signed bundle)
8
+ * quox wazuh report verify <id|file> [--public-key <pem|base64|file>]
9
+ *
10
+ * `verify <id>` asks the collector, which checks against its pinned WARD key (trust anchored).
11
+ * `verify <file>` is fully OFFLINE: it recomputes sha256 over the canonical report and checks the Ed25519
12
+ * signature with node:crypto. Against the key embedded in the file this proves integrity only (anyone can
13
+ * mint a keypair); pass --public-key (a PEM, a PEM file, or the raw 32 byte base64 WARD key) to pin the signer.
14
+ *
15
+ * Exit codes: 0 ok / verified, 1 error, 2 verification FAILED.
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
19
+ import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
20
+ import { resolveEndpoint } from '../api/endpoints.js';
21
+ import { canonicalize } from './kumaSla.js';
22
+
23
+ const KIND = 'quox.wazuh.posture.report';
24
+ const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
25
+
26
+ /** A public key from a PEM string, a path to a PEM file, or a raw 32 byte base64 Ed25519 key. null if none of those. */
27
+ export function parsePublicKey(value) {
28
+ if (!value) return null;
29
+ let text = String(value);
30
+ try { if (!/BEGIN PUBLIC KEY/.test(text) && existsSync(text)) text = readFileSync(text, 'utf-8'); } catch { /* not a file */ }
31
+ try {
32
+ if (/BEGIN PUBLIC KEY/.test(text)) {
33
+ const k = createPublicKey(text);
34
+ return k.asymmetricKeyType === 'ed25519' ? k : null;
35
+ }
36
+ const raw = Buffer.from(text.trim(), 'base64');
37
+ if (raw.length !== 32) return null;
38
+ return createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: 'der', type: 'spki' });
39
+ } catch { return null; }
40
+ }
41
+
42
+ /** Offline verification of an export bundle. Mirrors services/collector/lib/wazuhPostureReports.js verifyBundle. */
43
+ export function verifyBundleOffline(bundle, { publicKey } = {}) {
44
+ const out = { valid: false, hash_ok: false, binding_ok: false, signature_ok: false, key_source: 'none', trust_anchored: false };
45
+ if (!bundle || typeof bundle !== 'object' || !bundle.report || typeof bundle.report !== 'object') return { ...out, reason: 'malformed_bundle' };
46
+ const sig = bundle.signature;
47
+ if (!sig || !sig.signature || !sig.message) return { ...out, reason: 'missing_signature' };
48
+ const recomputed = createHash('sha256').update(canonicalize(bundle.report), 'utf-8').digest('hex');
49
+ out.recomputed_sha256 = recomputed;
50
+ out.hash_ok = recomputed === bundle.report_sha256;
51
+ if (!out.hash_ok) return { ...out, reason: 'report_hash_mismatch' };
52
+ const r = bundle.report;
53
+ const expected = { kind: KIND, report_id: bundle.report_id, org_id: r.org_id, scope: r.scope, generated_at: r.generated_at, report_sha256: recomputed };
54
+ out.binding_ok = canonicalize(expected) === canonicalize(sig.message);
55
+ if (!out.binding_ok) return { ...out, reason: 'signature_not_bound_to_report' };
56
+ if (sig.sig_type !== 'ed25519') return { ...out, reason: `unverifiable_sig_type:${sig.sig_type}` };
57
+ let key = null; let source = 'none';
58
+ if (publicKey) { key = parsePublicKey(publicKey); source = 'pinned'; if (!key) return { ...out, reason: 'bad_public_key' }; }
59
+ else { key = parsePublicKey(sig.public_key); source = 'embedded'; }
60
+ if (!key) return { ...out, reason: 'no_verification_key' };
61
+ out.key_source = source;
62
+ try {
63
+ out.signature_ok = cryptoVerify(null, Buffer.from(canonicalize(sig.message), 'utf-8'), key, Buffer.from(String(sig.signature), 'base64'));
64
+ } catch (err) { return { ...out, reason: `verify_error:${err.message}` }; }
65
+ if (!out.signature_ok) return { ...out, reason: 'signature_mismatch' };
66
+ out.valid = true;
67
+ out.trust_anchored = source === 'pinned' && !sig.warning;
68
+ if (sig.warning) out.warning = sig.warning;
69
+ return out;
70
+ }
71
+
72
+ const bundleOf = (d) => ({ report_id: d.report_id, report: d.report, report_sha256: d.report_sha256, signature: d.signature });
73
+
74
+ /** Human summary of a report: one line per section with its source status. Unavailable is said, never shown as zero. */
75
+ function printReport(d, pad) {
76
+ const r = d.report || {};
77
+ const s = r.sections || {};
78
+ console.log(`Report ${d.report_id} sha256 ${d.report_sha256}`);
79
+ console.log(`Org ${r.org_id} scope ${r.scope} manager ${r.wazuh_manager || 'unknown'} generated ${r.generated_at}`);
80
+ const st = (x) => (x ? x.status : 'missing');
81
+ const rows = [['SECTION', 'SOURCE', 'SUMMARY']];
82
+ const m = s.manager;
83
+ if (m) rows.push(['manager', st(m), m.status === 'unavailable' ? m.error : `${m.running} daemons running${m.stopped && m.stopped.length ? `, stopped: ${m.stopped.join(', ')}` : ''}; agents ${m.agent_counts ? `${m.agent_counts.active ?? '?'} active of ${m.agent_counts.total ?? '?'}` : 'unknown'}`]);
84
+ const c = s.coverage;
85
+ if (c) rows.push(['coverage', st(c), c.status === 'unavailable' ? c.error : `${c.summary.covered ?? 'unknown'} of ${c.summary.fleet_hosts ?? 'unknown'} fleet hosts covered, ${c.summary.missing_agent ?? 'unknown'} missing an agent, ${c.summary.disconnected} disconnected`]);
86
+ const v = s.vulnerabilities;
87
+ if (v) rows.push(['vulnerabilities', st(v), v.status === 'unavailable' ? v.error : `${v.summary.cves} CVEs, ${v.summary.in_kev} in CISA KEV, top ${v.items.length} listed`]);
88
+ const t = s.alert_triage;
89
+ if (t) rows.push(['alert triage', st(t), t.status === 'unavailable' ? t.error : `${t.total_alerts} alerts level ${t.min_level}+ in ${t.window}, ${t.groups.length} rule groups`]);
90
+ const sca = s.sca;
91
+ if (sca) rows.push(['sca', st(sca), sca.status === 'unavailable' ? sca.error : `${sca.policies.length} policies on agent ${sca.agent_id}`]);
92
+ console.log(`\n${pad(rows).join('\n')}`);
93
+ if (v && v.status !== 'unavailable' && v.items.length) {
94
+ const vr = [['CVE', 'KEV', 'EPSS', 'CVSS', 'HOSTS']];
95
+ for (const it of v.items) vr.push([it.cve, it.kev ? (it.kev_ransomware ? 'yes (ransomware)' : 'yes') : 'no', typeof it.epss === 'number' ? `${(it.epss * 100).toFixed(1)}%` : '-', it.cvss ?? '-', it.agents_affected ?? '-']);
96
+ console.log(`\nTop CVEs\n${pad(vr).join('\n')}`);
97
+ }
98
+ if (Array.isArray(r.not_included) && r.not_included.length) console.log(`\nNot included: ${r.not_included.map((n) => `${n.section} (${n.reason})`).join('; ')}`);
99
+ if (Array.isArray(r.cannot_establish) && r.cannot_establish.length) console.log(`Cannot establish:\n${r.cannot_establish.map((x) => ` - ${x}`).join('\n')}`);
100
+ }
101
+
102
+ export function registerWazuhReport(wazuh, { client, auth, jsonOut, guard, WazFail, pad, head, failFrom, example }) {
103
+ const call = async (verb, key, params, fn) => {
104
+ const ep = resolveEndpoint(key, params);
105
+ try { return { ep, data: await fn(ep) }; } catch (err) { throw failFrom(verb, ep.method, ep.path, err); }
106
+ };
107
+ const writeOut = (file, d) => writeFileSync(file, `${JSON.stringify(bundleOf(d), null, 2)}\n`);
108
+
109
+ const report = wazuh.command('report')
110
+ .description('Signed, offline-verifiable Wazuh security posture report: generate, list, show, verify')
111
+ .addHelpText('after', '\nExit codes: 0 ok, 1 error, 2 `report verify` found the report does NOT verify.');
112
+
113
+ example(report.command('generate')
114
+ .description('Build, sign and store a posture report (manager, coverage, CVE priority, 24h alert triage; SCA with --agent)')
115
+ .option('--agent <id|name>', 'scope the vulnerabilities (and add SCA) to one agent')
116
+ .option('--out <file>', 'also write the signed bundle here for offline verification')
117
+ .action(guard(async (options) => {
118
+ auth.requireAuth();
119
+ const body = {};
120
+ if (options.agent) body.agent = String(options.agent);
121
+ const { ep, data } = await call('report generate', 'mon.wazuh.reports.create', {}, (e) => client.post(e.path, { service: e.service, body, timeout: 180000, retries: 0 }));
122
+ if (!data || data.success === false || data.error || !data.report || !data.signature) {
123
+ throw new WazFail(`quox wazuh report generate: ${ep.method} ${ep.path} returned an error body: ${(data && (data.error || data.message)) || 'response had no report'}`);
124
+ }
125
+ if (options.out) writeOut(options.out, data);
126
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
127
+ const un = data.report.sections_unavailable || [];
128
+ head('report generate', `${data.report.scope}${un.length ? ` · ${un.length} section${un.length === 1 ? '' : 's'} unavailable` : ''}`, un.length || !data.trust_anchored ? 'warn' : 'ok');
129
+ printReport(data, pad);
130
+ console.log(data.trust_anchored ? '\nSigned with the WARD key and witnessed.' : '\nWARNING: signed with an ephemeral key (no persistent WARD key on this collector): integrity only, not trust anchored.');
131
+ console.log(`Verify: quox wazuh report verify ${options.out || data.report_id}`);
132
+ })), 'quox wazuh report generate --out posture.json');
133
+
134
+ example(report.command('list')
135
+ .description('This org\'s stored posture reports')
136
+ .action(guard(async () => {
137
+ auth.requireAuth();
138
+ const { ep, data } = await call('report list', 'mon.wazuh.reports.list', {}, (e) => client.get(e.path, { service: e.service }));
139
+ if (!data || data.success === false || data.error || !Array.isArray(data.reports)) {
140
+ throw new WazFail(`quox wazuh report list: ${ep.method} ${ep.path} returned an error body: ${(data && data.error) || 'response had no reports array'}`);
141
+ }
142
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
143
+ head('report list', `${data.reports.length} ${data.reports.length === 1 ? 'report' : 'reports'}`);
144
+ if (!data.reports.length) { console.log('No reports yet. Generate one: quox wazuh report generate'); return; }
145
+ const rows = [['REPORT', 'SCOPE', 'CREATED', 'SHA256']];
146
+ for (const r of data.reports) rows.push([r.report_id, r.scope, r.created_at, String(r.report_sha256).slice(0, 16)]);
147
+ console.log(pad(rows).join('\n'));
148
+ })), 'quox wazuh report list');
149
+
150
+ example(report.command('show')
151
+ .description('One stored posture report')
152
+ .argument('<id>', 'a report id from `quox wazuh report list`')
153
+ .option('--out <file>', 'write the signed bundle here for offline verification')
154
+ .action(guard(async (id, options) => {
155
+ auth.requireAuth();
156
+ const { ep, data } = await call('report show', 'mon.wazuh.reports.get', { id }, (e) => client.get(e.path, { service: e.service }));
157
+ if (!data || data.success === false || data.error || !data.report || !data.signature) {
158
+ throw new WazFail(`quox wazuh report show: ${ep.method} ${ep.path} returned an error body: ${(data && data.error) || 'response had no report'}`);
159
+ }
160
+ if (options.out) writeOut(options.out, data);
161
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
162
+ head('report show', data.report.scope);
163
+ printReport(data, pad);
164
+ })), 'quox wazuh report show WPR_ABC --out posture.json');
165
+
166
+ example(report.command('verify')
167
+ .description('Recompute the report hash and check the signature (id: via the collector, pinned WARD key; file: fully offline)')
168
+ .argument('<id|file>', 'a report id, or a bundle file written by `report generate --out` / `report show --out`')
169
+ .option('--public-key <pem|base64|file>', 'offline: pin this Ed25519 WARD public key (PEM, PEM file, or raw 32 byte base64) instead of trusting the key in the file')
170
+ .action(guard(async (target, options) => {
171
+ let result;
172
+ if (existsSync(target)) {
173
+ let bundle;
174
+ try { bundle = JSON.parse(readFileSync(target, 'utf-8')); } catch (err) { throw new WazFail(`quox wazuh report verify: ${target} is not valid JSON: ${err.message}`); }
175
+ result = { report_id: bundle && bundle.report_id, mode: 'offline', ...verifyBundleOffline(bundle, { publicKey: options.publicKey }) };
176
+ } else {
177
+ auth.requireAuth();
178
+ const { ep, data } = await call('report verify', 'mon.wazuh.reports.verify', { id: target }, (e) => client.get(e.path, { service: e.service }));
179
+ if (!data || data.success === false || data.error || typeof data.valid !== 'boolean') {
180
+ throw new WazFail(`quox wazuh report verify: ${ep.method} ${ep.path} returned an error body: ${(data && data.error) || 'response had no verdict'}`);
181
+ }
182
+ result = { mode: 'collector', ...data };
183
+ }
184
+ if (jsonOut()) { console.log(JSON.stringify(result, null, 2)); } else {
185
+ head('report verify', result.valid ? `verified (${result.mode})` : `NOT verified: ${result.reason}`, result.valid ? 'ok' : 'fail');
186
+ console.log(`hash ${result.hash_ok ? 'matches' : 'DOES NOT match'} binding ${result.binding_ok ? 'ok' : 'FAILED'} signature ${result.signature_ok ? 'valid' : 'INVALID'} key ${result.key_source}${result.trust_anchored ? ' (trust anchored)' : ' (not trust anchored)'}`);
187
+ if (result.valid && !result.trust_anchored) console.log('Integrity holds, but the signer was not checked against the WARD key. Pin it with --public-key, or verify by id through the collector.');
188
+ if (result.warning) console.log(`Warning: ${result.warning}`);
189
+ }
190
+ if (!result.valid) process.exitCode = 2;
191
+ })), 'quox wazuh report verify posture.json --public-key ward-pub.pem');
192
+ }
@@ -11,6 +11,23 @@ import { resolveOrgId, parseJSON } from './helpers.js';
11
11
  import { resolveEndpoint } from '../api/endpoints.js';
12
12
  import { confirmDestructive } from '../core/confirm.js';
13
13
 
14
+ // QUOXON (owner decision 2026-11-05, "opt-in yes"): a created workflow is not armed unless --activate was given.
15
+ // Say which, from the engine's own answer (active / trigger_state), never a guess. Silent in --json mode.
16
+ function printArmState(created, fallbackId) {
17
+ if (getOutputFormat() === 'json' || !created || typeof created !== 'object') return;
18
+ const id = created.id || fallbackId;
19
+ const ts = created.trigger_state && typeof created.trigger_state === 'object' ? created.trigger_state : null;
20
+ const active = ts && typeof ts.active === 'boolean' ? ts.active : created.active;
21
+ if (typeof active !== 'boolean') return;
22
+ const nodes = created.definition?.nodes || created.nodes || [];
23
+ const hasTrigger = Array.isArray(nodes) && nodes.some((n) => n && typeof n.type === 'string' && n.type.startsWith('trigger:'));
24
+ if (active) {
25
+ printInfo(ts?.next_run_at ? `Active: its trigger is armed, next run ${ts.next_run_at}.` : 'Active: its trigger is armed.');
26
+ } else if (hasTrigger) {
27
+ printInfo(`NOT active: its trigger is not armed, so it will not run on its own. Arm it with: quox trigger workflow-activate ${id}`);
28
+ }
29
+ }
30
+
14
31
  // Top-level Workflow fields compared for `workflow apply`'s diff-preview
15
32
  // (mirrors core/workflow.ts's WorkflowSchema field set, minus `id`).
16
33
  const APPLY_DIFF_FIELDS = ['name', 'description', 'notes', 'version', 'nodes', 'edges', 'variables', 'credentials', 'settings', 'metadata'];
@@ -242,10 +259,11 @@ export function registerWorkflowCommands(program, ctx) {
242
259
 
243
260
  workflow
244
261
  .command('create')
245
- .description('Create a new workflow in the workflow engine (quoxflow). --steps takes a JSON array of nodes (chained in order) or an object with nodes and optional edges')
262
+ .description('Create a new workflow in the workflow engine (quoxflow). --steps takes a JSON array of nodes (chained in order) or an object with nodes and optional edges. The workflow is created NOT active (its schedule or trigger is not armed) unless you pass --activate; arm it later with `quox trigger workflow-activate <id>`')
246
263
  .requiredOption('--name <name>', 'workflow name')
247
264
  .option('--description <desc>', 'description')
248
265
  .option('--steps <json>', 'nodes as a JSON array (chained sequentially), or {"nodes":[...],"edges":[...]}')
266
+ .option('--activate', 'arm the workflow now: register its schedule/webhook/event trigger (default: created inactive)')
249
267
  .action(async (options) => {
250
268
  auth.requireAuth();
251
269
  // #1190: the engine is the one workflow store. Same id shape as `workflow apply`.
@@ -264,12 +282,13 @@ export function registerWorkflowCommands(program, ctx) {
264
282
  throw new Error('--steps must be a JSON array of nodes, or an object with a "nodes" array (and optional "edges").');
265
283
  }
266
284
  }
267
- const body = { id, name: options.name, nodes, edges };
285
+ const body = { id, name: options.name, nodes, edges, activate: options.activate === true };
268
286
  if (options.description) body.description = options.description;
269
287
  const ep = resolveEndpoint('workflows.applyCreate');
270
288
  const res = await client.post(ep.path, { service: ep.service, body });
271
289
  const created = res?.data || res;
272
290
  printEvent(events.created('quox', 'workflow', `${options.name} (quoxflow) id:${created.id || id}`));
291
+ printArmState(created, id);
273
292
  // #1000: --json gets the saved workflow (the event line is suppressed in json mode, which left nothing)
274
293
  if (getOutputFormat() === 'json') formatDetail(created, { format: 'json' });
275
294
  });
@@ -288,7 +307,22 @@ export function registerWorkflowCommands(program, ctx) {
288
307
  if (options.name) body.name = options.name;
289
308
  if (options.description) body.description = options.description;
290
309
  if (options.status) body.status = options.status;
291
- const data = await client.patch(resolveEndpoint('workflows.update', { orgId, workflowId }).path, { service: resolveEndpoint('workflows.update', { orgId, workflowId }).service, body });
310
+ // Same order as `workflow get`/`delete`: `list` shows engine (quoxflow) workflows, so try the engine first and
311
+ // only fall back to the auth store on a 404. Any other engine error (403 etc.) is surfaced, never masked.
312
+ let data;
313
+ if (options.status === undefined) {
314
+ try {
315
+ const ep = resolveEndpoint('workflows.applyUpdate', { workflowId });
316
+ const res = await client.put(ep.path, { service: ep.service, body });
317
+ data = res?.data || res;
318
+ } catch (err) {
319
+ if (err.status !== 404) throw err;
320
+ }
321
+ }
322
+ if (data === undefined) {
323
+ const ep = resolveEndpoint('workflows.update', { orgId, workflowId });
324
+ data = await client.patch(ep.path, { service: ep.service, body });
325
+ }
292
326
  printEvent(events.updated('quox', 'workflow', workflowId));
293
327
  if (getOutputFormat() === 'json') formatDetail(data?.workflow || data, { format: 'json' });
294
328
  });
@@ -485,6 +519,7 @@ export function registerWorkflowCommands(program, ctx) {
485
519
  .description('Create or update a workflow declaratively from a YAML/JSON file (quoxflow-direct: POST if the file has no id, PUT with a diff-preview if it does)')
486
520
  .requiredOption('-f, --file <path>', 'path to a workflow definition file (.yaml/.yml/.json)')
487
521
  .option('--force', 'skip the diff-preview confirmation on update')
522
+ .option('--activate', 'when this creates the workflow, arm its trigger now (default: created inactive). An update never changes whether it is armed: use `quox trigger workflow-activate|workflow-deactivate`')
488
523
  .action(async (options) => {
489
524
  auth.requireAuth();
490
525
  const wf = readDeclarativeFile(options.file);
@@ -538,10 +573,13 @@ export function registerWorkflowCommands(program, ctx) {
538
573
  const createEp = resolveEndpoint('workflows.applyCreate');
539
574
  // #1000: quoxflow's WorkflowSchema requires an id, so "no id = create" 400'd. Mint one in the
540
575
  // engine's own wf-<ms>-<6> shape; the created id is printed, so the file can be given it to update later.
541
- const body = wf.id ? wf : { ...wf, id: `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8).padEnd(6, '0')}` };
576
+ const minted = wf.id ? wf : { ...wf, id: `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8).padEnd(6, '0')}` };
577
+ // Owner decision 2026-11-05: arming is opt-in; a file's own `activate` key is not an opt-in, only --activate is.
578
+ const body = { ...minted, activate: options.activate === true };
542
579
  const data = await client.post(createEp.path, { service: createEp.service, body });
543
580
  const created = data.data || data;
544
581
  printEvent(events.created('quox', 'workflow', `${created.name || wf.name || created.id} (quoxflow) id:${created.id}`));
582
+ printArmState(created, body.id);
545
583
  formatDetail(created, { format: program.opts().output });
546
584
  });
547
585
 
@@ -600,6 +638,7 @@ export function registerWorkflowCommands(program, ctx) {
600
638
  .requiredOption('-f, --file <path>', 'path to an n8n workflow export (.yaml/.yml/.json)')
601
639
  .option('--generate-ids', 'generate new node/workflow ids instead of reusing the n8n ones')
602
640
  .option('--include-positions', 'preserve n8n canvas node positions')
641
+ .option('--activate', 'arm the imported workflow\'s trigger now (default: imported inactive)')
603
642
  .action(async (options) => {
604
643
  auth.requireAuth();
605
644
  const parsed = readDeclarativeFile(options.file);
@@ -608,6 +647,9 @@ export function registerWorkflowCommands(program, ctx) {
608
647
  const importOptions = { ...(parsed?.options || {}) };
609
648
  if (options.generateIds) importOptions.generateIds = true;
610
649
  if (options.includePositions) importOptions.includePositions = true;
650
+ // Owner decision 2026-11-05: only --activate arms an import (a wrapped file's own options.activate does not).
651
+ delete importOptions.activate;
652
+ if (options.activate) importOptions.activate = true;
611
653
 
612
654
  const ep = resolveEndpoint('workflows.importN8n');
613
655
  const data = await client.post(ep.path, {
@@ -616,6 +658,7 @@ export function registerWorkflowCommands(program, ctx) {
616
658
  });
617
659
  const result = data.data || data;
618
660
  printEvent(glyphLine('quox', 'n8n workflow imported', { result: result.workflow?.id || result.id, status: 'ok' }));
661
+ printArmState(result.workflow, result.workflow?.id || result.id);
619
662
  if (result.warnings?.length) {
620
663
  for (const w of result.warnings) printWarning(w);
621
664
  }