@ciphrix/cli 0.1.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 (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +201 -0
  3. package/dist/actions.d.ts +19 -0
  4. package/dist/actions.js +30 -0
  5. package/dist/api.d.ts +27 -0
  6. package/dist/api.js +180 -0
  7. package/dist/banner.d.ts +2 -0
  8. package/dist/banner.js +12 -0
  9. package/dist/browser.d.ts +2 -0
  10. package/dist/browser.js +44 -0
  11. package/dist/cli.d.ts +14 -0
  12. package/dist/cli.js +1579 -0
  13. package/dist/commands/asset.d.ts +59 -0
  14. package/dist/commands/asset.js +237 -0
  15. package/dist/commands/check.d.ts +30 -0
  16. package/dist/commands/check.js +182 -0
  17. package/dist/commands/context.d.ts +23 -0
  18. package/dist/commands/context.js +102 -0
  19. package/dist/commands/control.d.ts +44 -0
  20. package/dist/commands/control.js +189 -0
  21. package/dist/commands/framework.d.ts +40 -0
  22. package/dist/commands/framework.js +181 -0
  23. package/dist/commands/job.d.ts +16 -0
  24. package/dist/commands/job.js +30 -0
  25. package/dist/commands/login.d.ts +12 -0
  26. package/dist/commands/login.js +54 -0
  27. package/dist/commands/logout.d.ts +9 -0
  28. package/dist/commands/logout.js +56 -0
  29. package/dist/commands/policy.d.ts +23 -0
  30. package/dist/commands/policy.js +112 -0
  31. package/dist/commands/policyActions.d.ts +37 -0
  32. package/dist/commands/policyActions.js +177 -0
  33. package/dist/commands/risk.d.ts +51 -0
  34. package/dist/commands/risk.js +289 -0
  35. package/dist/commands/test.d.ts +54 -0
  36. package/dist/commands/test.js +331 -0
  37. package/dist/commands/vendor.d.ts +45 -0
  38. package/dist/commands/vendor.js +218 -0
  39. package/dist/config.d.ts +12 -0
  40. package/dist/config.js +56 -0
  41. package/dist/constants.d.ts +7 -0
  42. package/dist/constants.js +7 -0
  43. package/dist/credentials.d.ts +19 -0
  44. package/dist/credentials.js +182 -0
  45. package/dist/deviceFlow.d.ts +32 -0
  46. package/dist/deviceFlow.js +115 -0
  47. package/dist/deviceInfo.d.ts +11 -0
  48. package/dist/deviceInfo.js +25 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +4 -0
  51. package/dist/io.d.ts +46 -0
  52. package/dist/io.js +99 -0
  53. package/dist/output.d.ts +14 -0
  54. package/dist/output.js +37 -0
  55. package/dist/theme.d.ts +25 -0
  56. package/dist/theme.js +39 -0
  57. package/dist/toolSurface.d.ts +20 -0
  58. package/dist/toolSurface.js +49 -0
  59. package/dist/upload.d.ts +18 -0
  60. package/dist/upload.js +115 -0
  61. package/package.json +71 -0
  62. package/skills/ciphrix/SKILL.md +169 -0
  63. package/skills/ciphrix/references/commands.md +1592 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1579 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { formatBanner } from './banner.js';
3
+ import { loginCommand } from './commands/login.js';
4
+ import { logoutCommand } from './commands/logout.js';
5
+ import { clauseAvailableItemsCommand, clauseGetCommand, clauseItemsCommand, clauseLinkCommand, clauseUpdateCommand, frameworkClausesCommand, frameworkListCommand, } from './commands/framework.js';
6
+ import { vendorAttachCommand, vendorCreateCommand, vendorDeleteCommand, vendorDetachCommand, vendorFilesCommand, vendorGetCommand, vendorListCommand, vendorUpdateCommand, } from './commands/vendor.js';
7
+ import { jobStatusCommand } from './commands/job.js';
8
+ import { assetCreateCommand, assetDeleteCommand, assetGetCommand, assetLinksCommand, assetListCommand, assetTagCommand, assetTagsCommand, assetUpdateCommand, } from './commands/asset.js';
9
+ import { controlAvailableItemsCommand, controlGetCommand, controlItemsCommand, controlLinkCommand, controlListCommand, controlUpdateCommand, } from './commands/control.js';
10
+ import { testAttachCommand, testAvailableItemsCommand, testDeleteCommand, testGetCommand, testItemsCommand, testLinksCommand, testListCommand, testRunCreateCommand, testRunDeleteCommand, testRunResultCommand, testRunsCommand, testUpdateCommand, } from './commands/test.js';
11
+ import { contextGetCommand, contextResetCommand, contextSetCommand, contextStatusCommand, } from './commands/context.js';
12
+ import { policyGetCommand, policyListCommand } from './commands/policy.js';
13
+ import { policyApproveCommand, policyCreateCommand, policyDeleteCommand, policyEditCommand, policySubmitCommand, policyUpdateMetaCommand, policyVersionCommand, policyVersionsCommand, } from './commands/policyActions.js';
14
+ import { riskAttachCommand, riskControlsCommand, riskCreateCommand, riskDeleteCommand, riskDetachCommand, riskFilesCommand, riskGetCommand, riskLinkControlCommand, riskListCommand, riskScoresCommand, riskTreatmentCommand, riskUpdateCommand, } from './commands/risk.js';
15
+ import { checkFindingsCommand, checkIntegrationsCommand, checkListCommand, checkResourcesCommand, checkRunCommand, checkRunsCommand, checkSetEnabledCommand, } from './commands/check.js';
16
+ import { CLI_NAME, CLI_TAGLINE, CLI_VERSION } from './constants.js';
17
+ import { setCliInsecureHttpOptIn } from './config.js';
18
+ import { createDefaultIO, writeBlock, writeLine } from './io.js';
19
+ const readStringOption = (options, key) => {
20
+ if (!options || typeof options !== 'object')
21
+ return undefined;
22
+ const value = options[key];
23
+ return typeof value === 'string' ? value : undefined;
24
+ };
25
+ const readBooleanOption = (options, key) => {
26
+ if (!options || typeof options !== 'object')
27
+ return false;
28
+ return options[key] === true;
29
+ };
30
+ const readNumberOption = (options, key) => {
31
+ if (!options || typeof options !== 'object')
32
+ return undefined;
33
+ const value = options[key];
34
+ if (typeof value === 'number')
35
+ return value;
36
+ if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)))
37
+ return Number(value);
38
+ return undefined;
39
+ };
40
+ const apiUrlOf = (url, options) => url ?? readStringOption(options, 'apiUrl');
41
+ export const CONFIGURATION_HELP = `
42
+ Configuration:
43
+ --api-url <url> override the API base URL for one command
44
+ CIPHRIX_API_URL default API base URL for this process
45
+ CIPHRIX_CREDENTIAL_STORE=file use plaintext file credentials for local/headless testing
46
+ CIPHRIX_ALLOW_INSECURE_HTTP=true
47
+ permit HTTP for intentional local testing
48
+
49
+ Production and the OS keychain are used by default. No general config file is read.
50
+ See the README Configuration section for precedence and security details.
51
+ `;
52
+ /**
53
+ * Builds the command tree. Kept separate from `main` so tests can construct a
54
+ * program with captured output and no process exits.
55
+ */
56
+ export const createProgram = (io = createDefaultIO()) => {
57
+ const program = new Command();
58
+ program
59
+ .name(CLI_NAME)
60
+ .description(CLI_TAGLINE)
61
+ .version(CLI_VERSION, '-v, --version', 'output the version number')
62
+ .option('--allow-insecure-http', 'allow plaintext HTTP for intentional local or non-production testing only')
63
+ .addHelpText('after', CONFIGURATION_HELP)
64
+ .showHelpAfterError()
65
+ .showSuggestionAfterError();
66
+ // This is a root option, so it works before or after the subcommand. Scope its
67
+ // effect to the action; URL validation still happens before command side effects.
68
+ program.hook('preAction', (_thisCommand, actionCommand) => {
69
+ setCliInsecureHttpOptIn(actionCommand.optsWithGlobals().allowInsecureHttp === true);
70
+ });
71
+ program.hook('postAction', () => setCliInsecureHttpOptIn(false));
72
+ program
73
+ .command('login [url]')
74
+ .description('Sign in to Ciphrix with device authorization')
75
+ .option('--api-url <url>', 'override the API base URL for this command')
76
+ .option('--device-name <name>', 'a name for this device (defaults to the machine name)')
77
+ .option('--no-open', 'do not open the verification link in a browser')
78
+ .action(async (url, options) => {
79
+ await loginCommand({
80
+ io,
81
+ apiUrl: apiUrlOf(url, options),
82
+ deviceName: readStringOption(options, 'deviceName'),
83
+ noOpen: readBooleanOption(options, 'noOpen') ||
84
+ (options !== null &&
85
+ typeof options === 'object' &&
86
+ options.open === false),
87
+ });
88
+ });
89
+ program
90
+ .command('logout')
91
+ .description('Revoke the remote session and remove the stored credential')
92
+ .option('--api-url <url>', 'override the API base URL for this command')
93
+ .option('--local-only', 'remove the local credential without revoking the remote session')
94
+ .action(async (options) => {
95
+ await logoutCommand({
96
+ io,
97
+ apiUrl: readStringOption(options, 'apiUrl'),
98
+ localOnly: readBooleanOption(options, 'localOnly'),
99
+ });
100
+ });
101
+ const context = program
102
+ .command('context')
103
+ .description('Read and update Business / Operating Context');
104
+ context
105
+ .command('get <store>')
106
+ .description('Print a context digest (business | operating)')
107
+ .option('--api-url <url>', 'override the API base URL for this command')
108
+ .option('--json', 'output raw JSON')
109
+ .action(async (store, options) => {
110
+ await contextGetCommand({
111
+ io,
112
+ store,
113
+ apiUrl: readStringOption(options, 'apiUrl'),
114
+ json: readBooleanOption(options, 'json'),
115
+ });
116
+ });
117
+ context
118
+ .command('status <store>')
119
+ .description('Show context completion and unanswered questions')
120
+ .option('--api-url <url>', 'override the API base URL for this command')
121
+ .option('--json', 'output raw JSON')
122
+ .action(async (store, options) => {
123
+ await contextStatusCommand({
124
+ io,
125
+ store,
126
+ apiUrl: readStringOption(options, 'apiUrl'),
127
+ json: readBooleanOption(options, 'json'),
128
+ });
129
+ });
130
+ context
131
+ .command('set <target> <value>')
132
+ .description('Set one answer, for example business.company_size "50-200"')
133
+ .option('--api-url <url>', 'override the API base URL for this command')
134
+ .option('--json', 'output raw JSON')
135
+ .action(async (target, value, options) => {
136
+ await contextSetCommand({
137
+ io,
138
+ target,
139
+ value,
140
+ apiUrl: readStringOption(options, 'apiUrl'),
141
+ json: readBooleanOption(options, 'json'),
142
+ });
143
+ });
144
+ context
145
+ .command('reset')
146
+ .description('Reset Business Context (asks to confirm)')
147
+ .option('--yes', 'apply without prompting')
148
+ .option('--api-url <url>', 'override the API base URL for this command')
149
+ .option('--json', 'output raw JSON')
150
+ .action(async (options) => {
151
+ await contextResetCommand({
152
+ io,
153
+ yes: readBooleanOption(options, 'yes'),
154
+ apiUrl: readStringOption(options, 'apiUrl'),
155
+ json: readBooleanOption(options, 'json'),
156
+ });
157
+ });
158
+ const test = program.command('test').description('Tests, their runs, items and ownership');
159
+ test
160
+ .command('list')
161
+ .description('List tests with their latest run status')
162
+ .option('--search <text>', 'filter by name')
163
+ .option('--status <csv>', 'filter by latest run status')
164
+ .option('--page <n>', 'page number')
165
+ .option('--limit <n>', 'page size')
166
+ .option('--api-url <url>', 'override the API base URL for this command')
167
+ .option('--json', 'output raw JSON')
168
+ .action(async (options) => {
169
+ await testListCommand({
170
+ io,
171
+ search: readStringOption(options, 'search'),
172
+ status: readStringOption(options, 'status'),
173
+ page: readNumberOption(options, 'page'),
174
+ limit: readNumberOption(options, 'limit'),
175
+ apiUrl: readStringOption(options, 'apiUrl'),
176
+ json: readBooleanOption(options, 'json'),
177
+ });
178
+ });
179
+ test
180
+ .command('get <name>')
181
+ .description('Show a test, its runs and AI assurance')
182
+ .option('--api-url <url>', 'override the API base URL for this command')
183
+ .option('--json', 'output raw JSON')
184
+ .action(async (name, options) => {
185
+ await testGetCommand({
186
+ io,
187
+ name,
188
+ apiUrl: readStringOption(options, 'apiUrl'),
189
+ json: readBooleanOption(options, 'json'),
190
+ });
191
+ });
192
+ test
193
+ .command('runs <name>')
194
+ .description('List a test\u2019s runs with AI assurance')
195
+ .option('--api-url <url>', 'override the API base URL for this command')
196
+ .option('--json', 'output raw JSON')
197
+ .action(async (name, options) => {
198
+ await testRunsCommand({
199
+ io,
200
+ name,
201
+ apiUrl: readStringOption(options, 'apiUrl'),
202
+ json: readBooleanOption(options, 'json'),
203
+ });
204
+ });
205
+ test
206
+ .command('items <name>')
207
+ .description('List a run\u2019s evidence with AI relevance (defaults to the current run)')
208
+ .option('--run <id>', 'a specific run')
209
+ .option('--api-url <url>', 'override the API base URL for this command')
210
+ .option('--json', 'output raw JSON')
211
+ .action(async (name, options) => {
212
+ await testItemsCommand({
213
+ io,
214
+ name,
215
+ runId: readStringOption(options, 'run'),
216
+ apiUrl: readStringOption(options, 'apiUrl'),
217
+ json: readBooleanOption(options, 'json'),
218
+ });
219
+ });
220
+ test
221
+ .command('attach <name>')
222
+ .description('Attach evidence to a run (defaults to the current run)')
223
+ .option('--check <id>', 'link a check')
224
+ .option('--native-doc <id>', 'link a native document')
225
+ .option('--upload <id>', 'link a staged upload id')
226
+ .option('--file <path>', 'upload a local file and attach it')
227
+ .option('--run <id>', 'a specific run')
228
+ .option('--unlink', 'unlink instead of link')
229
+ .option('--api-url <url>', 'override the API base URL for this command')
230
+ .option('--json', 'output raw JSON')
231
+ .action(async (name, options) => {
232
+ const check = readStringOption(options, 'check');
233
+ const nativeDoc = readStringOption(options, 'nativeDoc');
234
+ const upload = readStringOption(options, 'upload');
235
+ const itemId = check ?? nativeDoc ?? upload;
236
+ const itemType = check ? 'check' : nativeDoc ? 'native_doc' : 'upload';
237
+ await testAttachCommand({
238
+ io,
239
+ name,
240
+ itemType,
241
+ itemId: check || nativeDoc ? itemId : undefined,
242
+ uploadIds: upload ? [upload] : undefined,
243
+ file: readStringOption(options, 'file'),
244
+ runId: readStringOption(options, 'run'),
245
+ unlink: readBooleanOption(options, 'unlink'),
246
+ apiUrl: readStringOption(options, 'apiUrl'),
247
+ json: readBooleanOption(options, 'json'),
248
+ });
249
+ });
250
+ const testRun = test.command('run').description('Create, result and delete runs');
251
+ testRun
252
+ .command('create <name>')
253
+ .description('Create a new pending run')
254
+ .option('--run-at <date>', 'run date (ISO); defaults to now')
255
+ .option('--api-url <url>', 'override the API base URL for this command')
256
+ .option('--json', 'output raw JSON')
257
+ .action(async (name, options) => {
258
+ await testRunCreateCommand({
259
+ io,
260
+ name,
261
+ runAt: readStringOption(options, 'runAt'),
262
+ apiUrl: readStringOption(options, 'apiUrl'),
263
+ json: readBooleanOption(options, 'json'),
264
+ });
265
+ });
266
+ testRun
267
+ .command('result <name>')
268
+ .description('Record a run result: passing | failing | skipped | pending (reopen)')
269
+ .requiredOption('--status <status>', 'passing | failing | skipped | pending')
270
+ .option('--note <text>', 'note for the run')
271
+ .option('--run <id>', 'a specific run')
272
+ .option('--api-url <url>', 'override the API base URL for this command')
273
+ .option('--json', 'output raw JSON')
274
+ .action(async (name, options) => {
275
+ await testRunResultCommand({
276
+ io,
277
+ name,
278
+ status: readStringOption(options, 'status') ?? '',
279
+ note: readStringOption(options, 'note'),
280
+ runId: readStringOption(options, 'run'),
281
+ apiUrl: readStringOption(options, 'apiUrl'),
282
+ json: readBooleanOption(options, 'json'),
283
+ });
284
+ });
285
+ testRun
286
+ .command('delete <name>')
287
+ .description('Delete a run')
288
+ .requiredOption('--run <id>', 'the run to delete')
289
+ .option('--yes', 'apply without prompting')
290
+ .option('--api-url <url>', 'override the API base URL for this command')
291
+ .option('--json', 'output raw JSON')
292
+ .action(async (name, options) => {
293
+ await testRunDeleteCommand({
294
+ io,
295
+ name,
296
+ runId: readStringOption(options, 'run'),
297
+ yes: readBooleanOption(options, 'yes'),
298
+ apiUrl: readStringOption(options, 'apiUrl'),
299
+ json: readBooleanOption(options, 'json'),
300
+ });
301
+ });
302
+ test
303
+ .command('available <name>')
304
+ .description('List items available to attach to a run (defaults to the current run)')
305
+ .requiredOption('--type <type>', 'item type: native_doc or check')
306
+ .option('--run <id>', 'a specific run')
307
+ .option('--search <text>', 'filter by item name')
308
+ .option('--api-url <url>', 'override the API base URL for this command')
309
+ .option('--json', 'output raw JSON')
310
+ .action(async (name, options) => {
311
+ await testAvailableItemsCommand({
312
+ io,
313
+ name,
314
+ runId: readStringOption(options, 'run'),
315
+ itemType: readStringOption(options, 'type') ?? 'native_doc',
316
+ search: readStringOption(options, 'search'),
317
+ apiUrl: readStringOption(options, 'apiUrl'),
318
+ json: readBooleanOption(options, 'json'),
319
+ });
320
+ });
321
+ test
322
+ .command('links <name>')
323
+ .description('Show the controls and clauses a test covers')
324
+ .option('--api-url <url>', 'override the API base URL for this command')
325
+ .option('--json', 'output raw JSON')
326
+ .action(async (name, options) => {
327
+ await testLinksCommand({
328
+ io,
329
+ name,
330
+ apiUrl: readStringOption(options, 'apiUrl'),
331
+ json: readBooleanOption(options, 'json'),
332
+ });
333
+ });
334
+ test
335
+ .command('update <name>')
336
+ .description('Change applicability, responsibility, assignee, frequency or auto-pass')
337
+ .option('--applicability <value>', 'in_scope or out_of_scope')
338
+ .option('--responsibility <value>', 'internal, third_party or shared')
339
+ .option('--assigned-to <id>', 'owner user id')
340
+ .option('--frequency <value>', 'none, monthly, quarterly, half-yearly or yearly')
341
+ .option('--next-run-at <date>', 'ISO date for the next run')
342
+ .option('--auto-pass <on|off>', 'auto-pass from checks')
343
+ .option('--rename <text>', 'new name (custom tests only)')
344
+ .option('--guidance <text>', 'new guidance (custom tests only)')
345
+ .option('--yes', 'apply without prompting')
346
+ .option('--api-url <url>', 'override the API base URL for this command')
347
+ .option('--json', 'output raw JSON')
348
+ .action(async (name, options) => {
349
+ const changes = {};
350
+ for (const [flag, field] of [
351
+ ['applicability', 'applicability'],
352
+ ['responsibility', 'responsibility'],
353
+ ['assignedTo', 'assignedTo'],
354
+ ['frequency', 'frequency'],
355
+ ['nextRunAt', 'nextRunAt'],
356
+ ['rename', 'name'],
357
+ ['guidance', 'guidance'],
358
+ ]) {
359
+ const value = readStringOption(options, flag);
360
+ if (value !== undefined)
361
+ changes[field] = value;
362
+ }
363
+ const autoPass = readStringOption(options, 'autoPass');
364
+ if (autoPass !== undefined)
365
+ changes.autoPassFromChecks = autoPass === 'on';
366
+ await testUpdateCommand({
367
+ io,
368
+ name,
369
+ changes,
370
+ yes: readBooleanOption(options, 'yes'),
371
+ apiUrl: readStringOption(options, 'apiUrl'),
372
+ json: readBooleanOption(options, 'json'),
373
+ });
374
+ });
375
+ test
376
+ .command('delete <name>')
377
+ .description('Delete a custom test')
378
+ .option('--yes', 'apply without prompting')
379
+ .option('--api-url <url>', 'override the API base URL for this command')
380
+ .option('--json', 'output raw JSON')
381
+ .action(async (name, options) => {
382
+ await testDeleteCommand({
383
+ io,
384
+ name,
385
+ yes: readBooleanOption(options, 'yes'),
386
+ apiUrl: readStringOption(options, 'apiUrl'),
387
+ json: readBooleanOption(options, 'json'),
388
+ });
389
+ });
390
+ const framework = program.command('framework').description('Applied frameworks');
391
+ framework
392
+ .command('list')
393
+ .description('List applied frameworks with status and available frameworks')
394
+ .option('--api-url <url>', 'override the API base URL for this command')
395
+ .option('--json', 'output raw JSON')
396
+ .action(async (options) => {
397
+ await frameworkListCommand({
398
+ io,
399
+ apiUrl: readStringOption(options, 'apiUrl'),
400
+ json: readBooleanOption(options, 'json'),
401
+ });
402
+ });
403
+ framework
404
+ .command('clauses <framework>')
405
+ .description('List a framework’s clauses')
406
+ .option('--search <text>', 'filter by clause code or name')
407
+ .option('--applicability <value>', 'in_scope or out_of_scope')
408
+ .option('--page <n>', 'page number')
409
+ .option('--limit <n>', 'page size')
410
+ .option('--api-url <url>', 'override the API base URL for this command')
411
+ .option('--json', 'output raw JSON')
412
+ .action(async (name, options) => {
413
+ await frameworkClausesCommand({
414
+ io,
415
+ framework: name,
416
+ search: readStringOption(options, 'search'),
417
+ applicability: readStringOption(options, 'applicability'),
418
+ page: readNumberOption(options, 'page'),
419
+ limit: readNumberOption(options, 'limit'),
420
+ apiUrl: readStringOption(options, 'apiUrl'),
421
+ json: readBooleanOption(options, 'json'),
422
+ });
423
+ });
424
+ framework
425
+ .command('clause <framework> <clause>')
426
+ .description('Show one clause by code or name')
427
+ .option('--api-url <url>', 'override the API base URL for this command')
428
+ .option('--json', 'output raw JSON')
429
+ .action(async (frameworkName, clause, options) => {
430
+ await clauseGetCommand({
431
+ io,
432
+ framework: frameworkName,
433
+ clause,
434
+ apiUrl: readStringOption(options, 'apiUrl'),
435
+ json: readBooleanOption(options, 'json'),
436
+ });
437
+ });
438
+ framework
439
+ .command('items <framework> <clause>')
440
+ .description('List the items mapped to a clause')
441
+ .option('--api-url <url>', 'override the API base URL for this command')
442
+ .option('--json', 'output raw JSON')
443
+ .action(async (frameworkName, clause, options) => {
444
+ await clauseItemsCommand({
445
+ io,
446
+ framework: frameworkName,
447
+ clause,
448
+ apiUrl: readStringOption(options, 'apiUrl'),
449
+ json: readBooleanOption(options, 'json'),
450
+ });
451
+ });
452
+ framework
453
+ .command('available <framework> <clause>')
454
+ .description('List items that can be mapped to a clause')
455
+ .requiredOption('--type <type>', 'item type: test or document')
456
+ .option('--search <text>', 'filter by item name')
457
+ .option('--api-url <url>', 'override the API base URL for this command')
458
+ .option('--json', 'output raw JSON')
459
+ .action(async (frameworkName, clause, options) => {
460
+ await clauseAvailableItemsCommand({
461
+ io,
462
+ framework: frameworkName,
463
+ clause,
464
+ itemType: readStringOption(options, 'type') ?? 'test',
465
+ search: readStringOption(options, 'search'),
466
+ apiUrl: readStringOption(options, 'apiUrl'),
467
+ json: readBooleanOption(options, 'json'),
468
+ });
469
+ });
470
+ framework
471
+ .command('update <framework> <clause>')
472
+ .description('Change a clause’s applicability or design-requirement assessment')
473
+ .option('--applicability <value>', 'in_scope or out_of_scope')
474
+ .option('--justification <text>', 'why the applicability changed')
475
+ .option('--design-requirements <state>', 'not_assessed | not_needed | not_met | partial | met')
476
+ .option('--yes', 'apply without prompting')
477
+ .option('--api-url <url>', 'override the API base URL for this command')
478
+ .option('--json', 'output raw JSON')
479
+ .action(async (frameworkName, clause, options) => {
480
+ const changes = {};
481
+ for (const [flag, field] of [
482
+ ['applicability', 'applicability'],
483
+ ['justification', 'justification'],
484
+ ['designRequirements', 'designRequirements'],
485
+ ]) {
486
+ const value = readStringOption(options, flag);
487
+ if (value !== undefined)
488
+ changes[field] = value;
489
+ }
490
+ await clauseUpdateCommand({
491
+ io,
492
+ framework: frameworkName,
493
+ clause,
494
+ changes,
495
+ yes: readBooleanOption(options, 'yes'),
496
+ apiUrl: readStringOption(options, 'apiUrl'),
497
+ json: readBooleanOption(options, 'json'),
498
+ });
499
+ });
500
+ framework
501
+ .command('link <framework> <clause> <itemId>')
502
+ .description('Map a test or document to a clause')
503
+ .requiredOption('--type <type>', 'item type: test or document')
504
+ .option('--unlink', 'unlink instead of link')
505
+ .option('--yes', 'apply without prompting')
506
+ .option('--api-url <url>', 'override the API base URL for this command')
507
+ .option('--json', 'output raw JSON')
508
+ .action(async (frameworkName, clause, itemId, options) => {
509
+ await clauseLinkCommand({
510
+ io,
511
+ framework: frameworkName,
512
+ clause,
513
+ itemId,
514
+ itemType: readStringOption(options, 'type') ?? 'test',
515
+ action: readBooleanOption(options, 'unlink') ? 'unlink' : 'link',
516
+ yes: readBooleanOption(options, 'yes'),
517
+ apiUrl: readStringOption(options, 'apiUrl'),
518
+ json: readBooleanOption(options, 'json'),
519
+ });
520
+ });
521
+ const asset = program.command('asset').description('The asset register');
522
+ asset
523
+ .command('list')
524
+ .description('List assets with their code, status and classification')
525
+ .option('--name <text>', 'filter by name')
526
+ .option('--category <value>', 'filter by category')
527
+ .option('--status <csv>', 'filter by status')
528
+ .option('--business-impact <value>', 'filter by business impact')
529
+ .option('--data-classification <value>', 'filter by data classification')
530
+ .option('--page <n>', 'page number')
531
+ .option('--limit <n>', 'page size')
532
+ .option('--api-url <url>', 'override the API base URL for this command')
533
+ .option('--json', 'output raw JSON')
534
+ .action(async (options) => {
535
+ await assetListCommand({
536
+ io,
537
+ name: readStringOption(options, 'name'),
538
+ category: readStringOption(options, 'category'),
539
+ status: readStringOption(options, 'status'),
540
+ businessImpact: readStringOption(options, 'businessImpact'),
541
+ dataClassification: readStringOption(options, 'dataClassification'),
542
+ page: readNumberOption(options, 'page'),
543
+ limit: readNumberOption(options, 'limit'),
544
+ apiUrl: readStringOption(options, 'apiUrl'),
545
+ json: readBooleanOption(options, 'json'),
546
+ });
547
+ });
548
+ asset
549
+ .command('get <asset>')
550
+ .description('Show one asset by code, customer key or name')
551
+ .option('--api-url <url>', 'override the API base URL for this command')
552
+ .option('--json', 'output raw JSON')
553
+ .action(async (name, options) => {
554
+ await assetGetCommand({
555
+ io,
556
+ name,
557
+ apiUrl: readStringOption(options, 'apiUrl'),
558
+ json: readBooleanOption(options, 'json'),
559
+ });
560
+ });
561
+ asset
562
+ .command('links <asset>')
563
+ .description('List the controls, risks, tests or checks mapped to an asset')
564
+ .requiredOption('--type <type>', 'controls, risks, tests or checks')
565
+ .option('--page <n>', 'page number')
566
+ .option('--limit <n>', 'page size')
567
+ .option('--api-url <url>', 'override the API base URL for this command')
568
+ .option('--json', 'output raw JSON')
569
+ .action(async (name, options) => {
570
+ await assetLinksCommand({
571
+ io,
572
+ name,
573
+ type: readStringOption(options, 'type') ?? 'controls',
574
+ page: readNumberOption(options, 'page'),
575
+ limit: readNumberOption(options, 'limit'),
576
+ apiUrl: readStringOption(options, 'apiUrl'),
577
+ json: readBooleanOption(options, 'json'),
578
+ });
579
+ });
580
+ asset
581
+ .command('update <asset>')
582
+ .description('Change asset fields')
583
+ .option('--name <text>', 'rename')
584
+ .option('--description <text>', 'description')
585
+ .option('--category <value>', 'category')
586
+ .option('--sub-category <value>', 'sub-category')
587
+ .option('--status <value>', 'status')
588
+ .option('--data-classification <value>', 'data classification')
589
+ .option('--business-impact <value>', 'business impact')
590
+ .option('--technical-owner <id>', 'technical owner user id')
591
+ .option('--business-owner <value>', 'business owner')
592
+ .option('--customer-asset-key <value>', 'the tenant\u2019s own key for this asset')
593
+ .option('--external-ref <value>', 'external reference id')
594
+ .option('--location <value>', 'location')
595
+ .option('--yes', 'apply without prompting')
596
+ .option('--api-url <url>', 'override the API base URL for this command')
597
+ .option('--json', 'output raw JSON')
598
+ .action(async (name, options) => {
599
+ const changes = {};
600
+ for (const [flag, field] of [
601
+ ['name', 'name'],
602
+ ['description', 'description'],
603
+ ['category', 'category'],
604
+ ['subCategory', 'subCategory'],
605
+ ['status', 'status'],
606
+ ['dataClassification', 'dataClassification'],
607
+ ['businessImpact', 'businessImpact'],
608
+ ['technicalOwner', 'technicalOwner'],
609
+ ['businessOwner', 'businessOwner'],
610
+ ['customerAssetKey', 'customerAssetKey'],
611
+ ['externalRef', 'externalRefId'],
612
+ ['location', 'location'],
613
+ ]) {
614
+ const value = readStringOption(options, flag);
615
+ if (value !== undefined)
616
+ changes[field] = value;
617
+ }
618
+ await assetUpdateCommand({
619
+ io,
620
+ name,
621
+ changes,
622
+ yes: readBooleanOption(options, 'yes'),
623
+ apiUrl: readStringOption(options, 'apiUrl'),
624
+ json: readBooleanOption(options, 'json'),
625
+ });
626
+ });
627
+ asset
628
+ .command('create')
629
+ .description('Create an asset')
630
+ .requiredOption('--name <name>', 'asset name')
631
+ .requiredOption('--category <value>', 'category')
632
+ .requiredOption('--sub-category <value>', 'sub-category')
633
+ .option('--customer-key <value>', 'the tenant\u2019s own key for this asset')
634
+ .option('--external-ref <value>', 'external reference id')
635
+ .option('--description <text>', 'description')
636
+ .option('--status <value>', 'status')
637
+ .option('--business-impact <value>', 'business impact')
638
+ .option('--data-classification <value>', 'data classification')
639
+ .option('--technical-owner <id>', 'technical owner user id')
640
+ .option('--business-owner <value>', 'business owner')
641
+ .option('--location <value>', 'location')
642
+ .option('--api-url <url>', 'override the API base URL for this command')
643
+ .option('--json', 'output raw JSON')
644
+ .action(async (options) => {
645
+ await assetCreateCommand({
646
+ io,
647
+ name: readStringOption(options, 'name') ?? '',
648
+ category: readStringOption(options, 'category') ?? '',
649
+ subCategory: readStringOption(options, 'subCategory') ?? '',
650
+ customerAssetKey: readStringOption(options, 'customerKey'),
651
+ externalRefId: readStringOption(options, 'externalRef'),
652
+ description: readStringOption(options, 'description'),
653
+ status: readStringOption(options, 'status'),
654
+ businessImpact: readStringOption(options, 'businessImpact'),
655
+ dataClassification: readStringOption(options, 'dataClassification'),
656
+ technicalOwner: readStringOption(options, 'technicalOwner'),
657
+ businessOwner: readStringOption(options, 'businessOwner'),
658
+ location: readStringOption(options, 'location'),
659
+ apiUrl: readStringOption(options, 'apiUrl'),
660
+ json: readBooleanOption(options, 'json'),
661
+ });
662
+ });
663
+ asset
664
+ .command('delete <asset>')
665
+ .description('Delete an asset')
666
+ .option('--yes', 'apply without prompting')
667
+ .option('--api-url <url>', 'override the API base URL for this command')
668
+ .option('--json', 'output raw JSON')
669
+ .action(async (name, options) => {
670
+ await assetDeleteCommand({
671
+ io,
672
+ name,
673
+ yes: readBooleanOption(options, 'yes'),
674
+ apiUrl: readStringOption(options, 'apiUrl'),
675
+ json: readBooleanOption(options, 'json'),
676
+ });
677
+ });
678
+ asset
679
+ .command('tags <asset>')
680
+ .description('List an asset\u2019s tags')
681
+ .option('--api-url <url>', 'override the API base URL for this command')
682
+ .option('--json', 'output raw JSON')
683
+ .action(async (name, options) => {
684
+ await assetTagsCommand({
685
+ io,
686
+ name,
687
+ apiUrl: readStringOption(options, 'apiUrl'),
688
+ json: readBooleanOption(options, 'json'),
689
+ });
690
+ });
691
+ asset
692
+ .command('tag <asset>')
693
+ .description('Assign a key/value tag to an asset, or remove one')
694
+ .option('--key <key>', 'tag key')
695
+ .option('--value <value>', 'tag value')
696
+ .option('--unlink <mappingId>', 'tag mapping id to remove')
697
+ .option('--yes', 'apply without prompting')
698
+ .option('--api-url <url>', 'override the API base URL for this command')
699
+ .option('--json', 'output raw JSON')
700
+ .action(async (name, options) => {
701
+ const mappingId = readStringOption(options, 'unlink');
702
+ const key = readStringOption(options, 'key');
703
+ const value = readStringOption(options, 'value');
704
+ if (!mappingId && (!key || !value)) {
705
+ throw new Error('Provide --key and --value to assign, or --unlink <mappingId> to remove');
706
+ }
707
+ await assetTagCommand({
708
+ io,
709
+ name,
710
+ key,
711
+ value,
712
+ mappingId,
713
+ yes: readBooleanOption(options, 'yes'),
714
+ apiUrl: readStringOption(options, 'apiUrl'),
715
+ json: readBooleanOption(options, 'json'),
716
+ });
717
+ });
718
+ const vendor = program.command('vendor').description('Manage vendors');
719
+ vendor
720
+ .command('list')
721
+ .description('List vendors with a total count')
722
+ .option('--name <text>', 'filter by name')
723
+ .option('--criticality <value>', 'filter by criticality')
724
+ .option('--relationship-status <value>', 'filter by relationship status')
725
+ .option('--page <n>', 'page number')
726
+ .option('--limit <n>', 'page size')
727
+ .option('--api-url <url>', 'override the API base URL for this command')
728
+ .option('--json', 'output raw JSON')
729
+ .action(async (options) => {
730
+ await vendorListCommand({
731
+ io,
732
+ name: readStringOption(options, 'name'),
733
+ criticality: readStringOption(options, 'criticality'),
734
+ relationshipStatus: readStringOption(options, 'relationshipStatus'),
735
+ page: readNumberOption(options, 'page'),
736
+ limit: readNumberOption(options, 'limit'),
737
+ apiUrl: readStringOption(options, 'apiUrl'),
738
+ json: readBooleanOption(options, 'json'),
739
+ });
740
+ });
741
+ vendor
742
+ .command('get <name>')
743
+ .description('Show one vendor')
744
+ .option('--api-url <url>', 'override the API base URL for this command')
745
+ .option('--json', 'output raw JSON')
746
+ .action(async (name, options) => {
747
+ await vendorGetCommand({
748
+ io,
749
+ name,
750
+ apiUrl: readStringOption(options, 'apiUrl'),
751
+ json: readBooleanOption(options, 'json'),
752
+ });
753
+ });
754
+ vendor
755
+ .command('create')
756
+ .description('Create a vendor')
757
+ .requiredOption('--name <name>', 'vendor name')
758
+ .option('--category <value>', 'category')
759
+ .option('--criticality <value>', 'criticality')
760
+ .option('--description <text>', 'description')
761
+ .option('--api-url <url>', 'override the API base URL for this command')
762
+ .option('--json', 'output raw JSON')
763
+ .action(async (options) => {
764
+ await vendorCreateCommand({
765
+ io,
766
+ name: readStringOption(options, 'name') ?? '',
767
+ category: readStringOption(options, 'category'),
768
+ criticality: readStringOption(options, 'criticality'),
769
+ description: readStringOption(options, 'description'),
770
+ apiUrl: readStringOption(options, 'apiUrl'),
771
+ json: readBooleanOption(options, 'json'),
772
+ });
773
+ });
774
+ vendor
775
+ .command('update <name>')
776
+ .description('Update vendor fields')
777
+ .option('--name <value>', 'rename')
778
+ .option('--website <value>', 'website')
779
+ .option('--description <value>', 'description')
780
+ .option('--category <value>', 'category')
781
+ .option('--criticality <value>', 'business criticality')
782
+ .option('--data-sensitivity <value>', 'public | internal | confidential | pii | phi')
783
+ .option('--relationship-status <value>', 'active | inactive | onboarding | terminated')
784
+ .option('--review-status <value>', 'pending | completed | expired')
785
+ .option('--internal-owner <value>', 'internal owner (email)')
786
+ .option('--api-url <url>', 'override the API base URL for this command')
787
+ .option('--json', 'output raw JSON')
788
+ .action(async (vendorName, options) => {
789
+ const changes = {};
790
+ const fields = [
791
+ 'name',
792
+ 'website',
793
+ 'description',
794
+ 'category',
795
+ 'criticality',
796
+ 'dataSensitivity',
797
+ 'relationshipStatus',
798
+ 'reviewStatus',
799
+ 'internalOwner',
800
+ ];
801
+ for (const field of fields) {
802
+ const value = readStringOption(options, field);
803
+ if (value !== undefined)
804
+ changes[field] = value;
805
+ }
806
+ await vendorUpdateCommand({
807
+ io,
808
+ name: vendorName,
809
+ changes,
810
+ apiUrl: readStringOption(options, 'apiUrl'),
811
+ json: readBooleanOption(options, 'json'),
812
+ });
813
+ });
814
+ vendor
815
+ .command('delete <name>')
816
+ .description('Delete a vendor')
817
+ .option('--yes', 'apply without prompting')
818
+ .option('--api-url <url>', 'override the API base URL for this command')
819
+ .option('--json', 'output raw JSON')
820
+ .action(async (name, options) => {
821
+ await vendorDeleteCommand({
822
+ io,
823
+ name,
824
+ yes: readBooleanOption(options, 'yes'),
825
+ apiUrl: readStringOption(options, 'apiUrl'),
826
+ json: readBooleanOption(options, 'json'),
827
+ });
828
+ });
829
+ const control = program
830
+ .command('control')
831
+ .description('Controls: status, applicability, owner and design requirements');
832
+ control
833
+ .command('list')
834
+ .description('List controls with their status, applicability and owner')
835
+ .option('--search <text>', 'filter by name or code')
836
+ .option('--domain <value>', 'filter by control domain')
837
+ .option('--status <csv>', 'filter by status')
838
+ .option('--applicability <value>', 'filter by applicability (in_scope|out_of_scope)')
839
+ .option('--page <n>', 'page number')
840
+ .option('--limit <n>', 'page size')
841
+ .option('--api-url <url>', 'override the API base URL for this command')
842
+ .option('--json', 'output raw JSON')
843
+ .action(async (options) => {
844
+ await controlListCommand({
845
+ io,
846
+ search: readStringOption(options, 'search'),
847
+ domain: readStringOption(options, 'domain'),
848
+ status: readStringOption(options, 'status'),
849
+ applicability: readStringOption(options, 'applicability'),
850
+ page: readNumberOption(options, 'page'),
851
+ limit: readNumberOption(options, 'limit'),
852
+ apiUrl: readStringOption(options, 'apiUrl'),
853
+ json: readBooleanOption(options, 'json'),
854
+ });
855
+ });
856
+ control
857
+ .command('get <control>')
858
+ .description('Show one control by name or code')
859
+ .option('--api-url <url>', 'override the API base URL for this command')
860
+ .option('--json', 'output raw JSON')
861
+ .action(async (name, options) => {
862
+ await controlGetCommand({
863
+ io,
864
+ name,
865
+ apiUrl: readStringOption(options, 'apiUrl'),
866
+ json: readBooleanOption(options, 'json'),
867
+ });
868
+ });
869
+ control
870
+ .command('items <control>')
871
+ .description('List the items linked to a control')
872
+ .option('--search <text>', 'filter by item name')
873
+ .option('--page <n>', 'page number')
874
+ .option('--limit <n>', 'page size')
875
+ .option('--api-url <url>', 'override the API base URL for this command')
876
+ .option('--json', 'output raw JSON')
877
+ .action(async (name, options) => {
878
+ await controlItemsCommand({
879
+ io,
880
+ name,
881
+ search: readStringOption(options, 'search'),
882
+ page: readNumberOption(options, 'page'),
883
+ limit: readNumberOption(options, 'limit'),
884
+ apiUrl: readStringOption(options, 'apiUrl'),
885
+ json: readBooleanOption(options, 'json'),
886
+ });
887
+ });
888
+ control
889
+ .command('available <control>')
890
+ .description('List items that can be linked to a control')
891
+ .requiredOption('--type <type>', 'item type: test or document')
892
+ .option('--search <text>', 'filter by item name')
893
+ .option('--page <n>', 'page number')
894
+ .option('--limit <n>', 'page size')
895
+ .option('--api-url <url>', 'override the API base URL for this command')
896
+ .option('--json', 'output raw JSON')
897
+ .action(async (name, options) => {
898
+ await controlAvailableItemsCommand({
899
+ io,
900
+ name,
901
+ itemType: readStringOption(options, 'type') ?? 'test',
902
+ search: readStringOption(options, 'search'),
903
+ page: readNumberOption(options, 'page'),
904
+ limit: readNumberOption(options, 'limit'),
905
+ apiUrl: readStringOption(options, 'apiUrl'),
906
+ json: readBooleanOption(options, 'json'),
907
+ });
908
+ });
909
+ control
910
+ .command('update <control>')
911
+ .description('Change owner, applicability, design requirements or notes')
912
+ .option('--owner <id|none>', 'owner user id, or none to clear')
913
+ .option('--applicability <value>', 'in_scope or out_of_scope')
914
+ .option('--justification <text>', 'why the applicability changed')
915
+ .option('--design-requirements <state>', 'not_assessed | not_needed | not_met | partial | met')
916
+ .option('--notes <text>', 'notes (Markdown)')
917
+ .option('--rename <text>', 'new name (custom controls only)')
918
+ .option('--yes', 'apply without prompting')
919
+ .option('--api-url <url>', 'override the API base URL for this command')
920
+ .option('--json', 'output raw JSON')
921
+ .action(async (name, options) => {
922
+ const changes = {};
923
+ const owner = readStringOption(options, 'owner');
924
+ if (owner !== undefined)
925
+ changes.ownerId = owner === 'none' ? null : owner;
926
+ for (const [flag, field] of [
927
+ ['applicability', 'applicability'],
928
+ ['justification', 'justification'],
929
+ ['designRequirements', 'designRequirements'],
930
+ ['notes', 'notes'],
931
+ ['rename', 'name'],
932
+ ]) {
933
+ const value = readStringOption(options, flag);
934
+ if (value !== undefined)
935
+ changes[field] = value;
936
+ }
937
+ await controlUpdateCommand({
938
+ io,
939
+ name,
940
+ changes,
941
+ yes: readBooleanOption(options, 'yes'),
942
+ apiUrl: readStringOption(options, 'apiUrl'),
943
+ json: readBooleanOption(options, 'json'),
944
+ });
945
+ });
946
+ control
947
+ .command('link <control> <itemId>')
948
+ .description('Link a test or document to a control')
949
+ .requiredOption('--type <type>', 'item type: test or document')
950
+ .option('--unlink', 'unlink instead of link')
951
+ .option('--yes', 'apply without prompting')
952
+ .option('--api-url <url>', 'override the API base URL for this command')
953
+ .option('--json', 'output raw JSON')
954
+ .action(async (name, itemId, options) => {
955
+ await controlLinkCommand({
956
+ io,
957
+ name,
958
+ itemId,
959
+ itemType: readStringOption(options, 'type') ?? 'test',
960
+ action: readBooleanOption(options, 'unlink') ? 'unlink' : 'link',
961
+ yes: readBooleanOption(options, 'yes'),
962
+ apiUrl: readStringOption(options, 'apiUrl'),
963
+ json: readBooleanOption(options, 'json'),
964
+ });
965
+ });
966
+ vendor
967
+ .command('files <name>')
968
+ .description('List the files attached to a vendor')
969
+ .option('--api-url <url>', 'override the API base URL for this command')
970
+ .option('--json', 'output raw JSON')
971
+ .action(async (name, options) => {
972
+ await vendorFilesCommand({
973
+ io,
974
+ name,
975
+ apiUrl: readStringOption(options, 'apiUrl'),
976
+ json: readBooleanOption(options, 'json'),
977
+ });
978
+ });
979
+ vendor
980
+ .command('attach <name>')
981
+ .description('Attach a file to a vendor')
982
+ .option('--file <path>', 'stage and attach a local file')
983
+ .option('--upload <id>', 'attach an already-staged upload')
984
+ .option('--yes', 'apply without prompting')
985
+ .option('--api-url <url>', 'override the API base URL for this command')
986
+ .option('--json', 'output raw JSON')
987
+ .action(async (name, options) => {
988
+ await vendorAttachCommand({
989
+ io,
990
+ name,
991
+ file: readStringOption(options, 'file'),
992
+ uploadId: readStringOption(options, 'upload'),
993
+ yes: readBooleanOption(options, 'yes'),
994
+ apiUrl: readStringOption(options, 'apiUrl'),
995
+ json: readBooleanOption(options, 'json'),
996
+ });
997
+ });
998
+ vendor
999
+ .command('detach <name> <mappingId>')
1000
+ .description('Detach a file from a vendor by its mapping id')
1001
+ .option('--yes', 'apply without prompting')
1002
+ .option('--api-url <url>', 'override the API base URL for this command')
1003
+ .option('--json', 'output raw JSON')
1004
+ .action(async (name, mappingId, options) => {
1005
+ await vendorDetachCommand({
1006
+ io,
1007
+ name,
1008
+ mappingId,
1009
+ yes: readBooleanOption(options, 'yes'),
1010
+ apiUrl: readStringOption(options, 'apiUrl'),
1011
+ json: readBooleanOption(options, 'json'),
1012
+ });
1013
+ });
1014
+ const document = program.command('document').description('Browse, read and change documents');
1015
+ document
1016
+ .command('list')
1017
+ .description('List documents with status and counts')
1018
+ .option('--name <text>', 'filter by name')
1019
+ .option('--status <csv>', 'filter by workflow status')
1020
+ .option('--page <n>', 'page number')
1021
+ .option('--limit <n>', 'page size')
1022
+ .option('--api-url <url>', 'override the API base URL for this command')
1023
+ .option('--json', 'output raw JSON')
1024
+ .action(async (options) => {
1025
+ await policyListCommand({
1026
+ io,
1027
+ apiUrl: readStringOption(options, 'apiUrl'),
1028
+ json: readBooleanOption(options, 'json'),
1029
+ name: readStringOption(options, 'name'),
1030
+ status: readStringOption(options, 'status'),
1031
+ page: readNumberOption(options, 'page'),
1032
+ limit: readNumberOption(options, 'limit'),
1033
+ });
1034
+ });
1035
+ document
1036
+ .command('get <document>')
1037
+ .description('Print a document as Markdown (by name or id)')
1038
+ .option('--version-number <n>', 'a specific version number')
1039
+ .option('--minor-number <n>', 'a specific minor version (with --version-number)')
1040
+ .option('--api-url <url>', 'override the API base URL for this command')
1041
+ .option('--json', 'output raw JSON')
1042
+ .action(async (policyName, options) => {
1043
+ await policyGetCommand({
1044
+ io,
1045
+ policy: policyName,
1046
+ version: readNumberOption(options, 'versionNumber'),
1047
+ minor: readNumberOption(options, 'minorNumber'),
1048
+ apiUrl: readStringOption(options, 'apiUrl'),
1049
+ json: readBooleanOption(options, 'json'),
1050
+ });
1051
+ });
1052
+ document
1053
+ .command('update <document>')
1054
+ .description('Update document metadata')
1055
+ .option('--name <value>', 'rename')
1056
+ .option('--description <value>', 'description')
1057
+ .option('--type <value>', 'document type')
1058
+ .option('--publish <on|off>', 'published state')
1059
+ .option('--api-url <url>', 'override the API base URL for this command')
1060
+ .option('--json', 'output raw JSON')
1061
+ .action(async (policyName, options) => {
1062
+ const changes = {};
1063
+ const name = readStringOption(options, 'name');
1064
+ const description = readStringOption(options, 'description');
1065
+ const type = readStringOption(options, 'type');
1066
+ const publish = readStringOption(options, 'publish');
1067
+ if (name !== undefined)
1068
+ changes.name = name;
1069
+ if (description !== undefined)
1070
+ changes.description = description;
1071
+ if (type !== undefined)
1072
+ changes.type = type;
1073
+ if (publish !== undefined)
1074
+ changes.publish = publish === 'on' || publish === 'true';
1075
+ await policyUpdateMetaCommand({
1076
+ io,
1077
+ policy: policyName,
1078
+ changes,
1079
+ apiUrl: readStringOption(options, 'apiUrl'),
1080
+ json: readBooleanOption(options, 'json'),
1081
+ });
1082
+ });
1083
+ document
1084
+ .command('versions <document>')
1085
+ .description('Show a document\u2019s version history')
1086
+ .option('--api-url <url>', 'override the API base URL for this command')
1087
+ .option('--json', 'output raw JSON')
1088
+ .action(async (policyName, options) => {
1089
+ await policyVersionsCommand({
1090
+ io,
1091
+ policy: policyName,
1092
+ apiUrl: readStringOption(options, 'apiUrl'),
1093
+ json: readBooleanOption(options, 'json'),
1094
+ });
1095
+ });
1096
+ document
1097
+ .command('create')
1098
+ .description('Create a new blank document')
1099
+ .requiredOption('--name <name>', 'policy name')
1100
+ .option('--type <type>', 'document type')
1101
+ .option('--description <text>', 'short description')
1102
+ .option('--api-url <url>', 'override the API base URL for this command')
1103
+ .option('--json', 'output raw JSON')
1104
+ .action(async (options) => {
1105
+ await policyCreateCommand({
1106
+ io,
1107
+ name: readStringOption(options, 'name') ?? '',
1108
+ type: readStringOption(options, 'type'),
1109
+ description: readStringOption(options, 'description'),
1110
+ apiUrl: readStringOption(options, 'apiUrl'),
1111
+ json: readBooleanOption(options, 'json'),
1112
+ });
1113
+ });
1114
+ document
1115
+ .command('edit <document>')
1116
+ .description('Replace the content of a draft document with Markdown')
1117
+ .option('--file <path>', 'read the new Markdown from a file')
1118
+ .option('--content <markdown>', 'new Markdown inline')
1119
+ .option('--yes', 'apply without prompting')
1120
+ .option('--api-url <url>', 'override the API base URL for this command')
1121
+ .option('--json', 'output raw JSON')
1122
+ .action(async (policyName, options) => {
1123
+ await policyEditCommand({
1124
+ io,
1125
+ policy: policyName,
1126
+ file: readStringOption(options, 'file'),
1127
+ content: readStringOption(options, 'content'),
1128
+ yes: readBooleanOption(options, 'yes'),
1129
+ apiUrl: readStringOption(options, 'apiUrl'),
1130
+ json: readBooleanOption(options, 'json'),
1131
+ });
1132
+ });
1133
+ document
1134
+ .command('version <document>')
1135
+ .description('Create a new draft version of a document')
1136
+ .option('--change-type <type>', 'change type')
1137
+ .option('--summary <text>', 'change summary')
1138
+ .option('--yes', 'apply without prompting')
1139
+ .option('--api-url <url>', 'override the API base URL for this command')
1140
+ .option('--json', 'output raw JSON')
1141
+ .action(async (policyName, options) => {
1142
+ await policyVersionCommand({
1143
+ io,
1144
+ policy: policyName,
1145
+ changeType: readStringOption(options, 'changeType'),
1146
+ summary: readStringOption(options, 'summary'),
1147
+ yes: readBooleanOption(options, 'yes'),
1148
+ apiUrl: readStringOption(options, 'apiUrl'),
1149
+ json: readBooleanOption(options, 'json'),
1150
+ });
1151
+ });
1152
+ document
1153
+ .command('submit <document>')
1154
+ .description('Submit a draft document for review')
1155
+ .option('--yes', 'apply without prompting')
1156
+ .option('--api-url <url>', 'override the API base URL for this command')
1157
+ .option('--json', 'output raw JSON')
1158
+ .action(async (policyName, options) => {
1159
+ await policySubmitCommand({
1160
+ io,
1161
+ policy: policyName,
1162
+ yes: readBooleanOption(options, 'yes'),
1163
+ apiUrl: readStringOption(options, 'apiUrl'),
1164
+ json: readBooleanOption(options, 'json'),
1165
+ });
1166
+ });
1167
+ document
1168
+ .command('approve <document>')
1169
+ .description('Approve a document awaiting approval')
1170
+ .option('--yes', 'apply without prompting')
1171
+ .option('--api-url <url>', 'override the API base URL for this command')
1172
+ .option('--json', 'output raw JSON')
1173
+ .action(async (policyName, options) => {
1174
+ await policyApproveCommand({
1175
+ io,
1176
+ policy: policyName,
1177
+ yes: readBooleanOption(options, 'yes'),
1178
+ apiUrl: readStringOption(options, 'apiUrl'),
1179
+ json: readBooleanOption(options, 'json'),
1180
+ });
1181
+ });
1182
+ document
1183
+ .command('delete <document>')
1184
+ .description('Delete a document')
1185
+ .option('--yes', 'apply without prompting')
1186
+ .option('--api-url <url>', 'override the API base URL for this command')
1187
+ .option('--json', 'output raw JSON')
1188
+ .action(async (policyName, options) => {
1189
+ await policyDeleteCommand({
1190
+ io,
1191
+ policy: policyName,
1192
+ yes: readBooleanOption(options, 'yes'),
1193
+ apiUrl: readStringOption(options, 'apiUrl'),
1194
+ json: readBooleanOption(options, 'json'),
1195
+ });
1196
+ });
1197
+ const risk = program.command('risk').description('Manage risks');
1198
+ risk
1199
+ .command('list')
1200
+ .description('List the risk register')
1201
+ .option('--status <csv>', 'filter by status')
1202
+ .option('--business-unit <value>', 'filter by business unit')
1203
+ .option('--page <n>', 'page number')
1204
+ .option('--limit <n>', 'page size')
1205
+ .option('--api-url <url>', 'override the API base URL for this command')
1206
+ .option('--json', 'output raw JSON')
1207
+ .action(async (options) => {
1208
+ await riskListCommand({
1209
+ io,
1210
+ status: readStringOption(options, 'status'),
1211
+ businessUnit: readStringOption(options, 'businessUnit'),
1212
+ page: readNumberOption(options, 'page'),
1213
+ limit: readNumberOption(options, 'limit'),
1214
+ apiUrl: readStringOption(options, 'apiUrl'),
1215
+ json: readBooleanOption(options, 'json'),
1216
+ });
1217
+ });
1218
+ risk
1219
+ .command('get <risk>')
1220
+ .description('Show one risk by title or id')
1221
+ .option('--api-url <url>', 'override the API base URL for this command')
1222
+ .option('--json', 'output raw JSON')
1223
+ .action(async (riskName, options) => {
1224
+ await riskGetCommand({
1225
+ io,
1226
+ risk: riskName,
1227
+ apiUrl: readStringOption(options, 'apiUrl'),
1228
+ json: readBooleanOption(options, 'json'),
1229
+ });
1230
+ });
1231
+ risk
1232
+ .command('update <risk>')
1233
+ .description('Update risk fields')
1234
+ .option('--status <value>', 'open | in_treatment | monitor | closed')
1235
+ .option('--title <value>', 'title')
1236
+ .option('--description <value>', 'description')
1237
+ .option('--owner <value>', 'owner')
1238
+ .option('--treatment-type <value>', 'treatment strategy')
1239
+ .option('--treatment-notes <text>', 'treatment notes (Markdown)')
1240
+ .option('--business-unit <value>', 'business unit')
1241
+ .option('--api-url <url>', 'override the API base URL for this command')
1242
+ .option('--json', 'output raw JSON')
1243
+ .action(async (riskName, options) => {
1244
+ const changes = {};
1245
+ for (const field of [
1246
+ 'status',
1247
+ 'title',
1248
+ 'description',
1249
+ 'owner',
1250
+ 'businessUnit',
1251
+ 'treatmentType',
1252
+ 'treatmentNotes',
1253
+ ]) {
1254
+ const value = readStringOption(options, field);
1255
+ if (value !== undefined)
1256
+ changes[field] = value;
1257
+ }
1258
+ await riskUpdateCommand({
1259
+ io,
1260
+ risk: riskName,
1261
+ changes,
1262
+ apiUrl: readStringOption(options, 'apiUrl'),
1263
+ json: readBooleanOption(options, 'json'),
1264
+ });
1265
+ });
1266
+ risk
1267
+ .command('create')
1268
+ .description('Create a risk')
1269
+ .requiredOption('--title <title>', 'risk title')
1270
+ .option('--category <value>', 'category')
1271
+ .option('--description <value>', 'description')
1272
+ .option('--api-url <url>', 'override the API base URL for this command')
1273
+ .option('--json', 'output raw JSON')
1274
+ .action(async (options) => {
1275
+ await riskCreateCommand({
1276
+ io,
1277
+ title: readStringOption(options, 'title') ?? '',
1278
+ category: readStringOption(options, 'category'),
1279
+ description: readStringOption(options, 'description'),
1280
+ apiUrl: readStringOption(options, 'apiUrl'),
1281
+ json: readBooleanOption(options, 'json'),
1282
+ });
1283
+ });
1284
+ risk
1285
+ .command('delete <risk>')
1286
+ .description('Delete a risk')
1287
+ .option('--yes', 'apply without prompting')
1288
+ .option('--api-url <url>', 'override the API base URL for this command')
1289
+ .option('--json', 'output raw JSON')
1290
+ .action(async (riskName, options) => {
1291
+ await riskDeleteCommand({
1292
+ io,
1293
+ risk: riskName,
1294
+ yes: readBooleanOption(options, 'yes'),
1295
+ apiUrl: readStringOption(options, 'apiUrl'),
1296
+ json: readBooleanOption(options, 'json'),
1297
+ });
1298
+ });
1299
+ risk
1300
+ .command('treatment <risk>')
1301
+ .description('Show a risk’s treatment strategy and notes')
1302
+ .option('--api-url <url>', 'override the API base URL for this command')
1303
+ .option('--json', 'output raw JSON')
1304
+ .action(async (risk, options) => {
1305
+ await riskTreatmentCommand({
1306
+ io,
1307
+ risk,
1308
+ apiUrl: readStringOption(options, 'apiUrl'),
1309
+ json: readBooleanOption(options, 'json'),
1310
+ });
1311
+ });
1312
+ risk
1313
+ .command('scores <risk>')
1314
+ .description('Show a risk’s scores')
1315
+ .option('--api-url <url>', 'override the API base URL for this command')
1316
+ .option('--json', 'output raw JSON')
1317
+ .action(async (risk, options) => {
1318
+ await riskScoresCommand({
1319
+ io,
1320
+ risk,
1321
+ apiUrl: readStringOption(options, 'apiUrl'),
1322
+ json: readBooleanOption(options, 'json'),
1323
+ });
1324
+ });
1325
+ risk
1326
+ .command('controls <risk>')
1327
+ .description('List the controls linked to a risk')
1328
+ .option('--api-url <url>', 'override the API base URL for this command')
1329
+ .option('--json', 'output raw JSON')
1330
+ .action(async (risk, options) => {
1331
+ await riskControlsCommand({
1332
+ io,
1333
+ risk,
1334
+ apiUrl: readStringOption(options, 'apiUrl'),
1335
+ json: readBooleanOption(options, 'json'),
1336
+ });
1337
+ });
1338
+ risk
1339
+ .command('link <risk>')
1340
+ .description('Link a control to a risk, or unlink a mapping')
1341
+ .option('--control <id>', 'control instance id to link')
1342
+ .option('--unlink <mappingId>', 'control mapping id to unlink')
1343
+ .option('--yes', 'apply without prompting')
1344
+ .option('--api-url <url>', 'override the API base URL for this command')
1345
+ .option('--json', 'output raw JSON')
1346
+ .action(async (risk, options) => {
1347
+ const mappingId = readStringOption(options, 'unlink');
1348
+ await riskLinkControlCommand({
1349
+ io,
1350
+ risk,
1351
+ controlInstanceId: readStringOption(options, 'control'),
1352
+ mappingId,
1353
+ action: mappingId ? 'unlink' : 'link',
1354
+ yes: readBooleanOption(options, 'yes'),
1355
+ apiUrl: readStringOption(options, 'apiUrl'),
1356
+ json: readBooleanOption(options, 'json'),
1357
+ });
1358
+ });
1359
+ risk
1360
+ .command('files <risk>')
1361
+ .description('List the files attached to a risk')
1362
+ .option('--api-url <url>', 'override the API base URL for this command')
1363
+ .option('--json', 'output raw JSON')
1364
+ .action(async (risk, options) => {
1365
+ await riskFilesCommand({
1366
+ io,
1367
+ risk,
1368
+ apiUrl: readStringOption(options, 'apiUrl'),
1369
+ json: readBooleanOption(options, 'json'),
1370
+ });
1371
+ });
1372
+ risk
1373
+ .command('attach <risk>')
1374
+ .description('Attach a file to a risk')
1375
+ .option('--file <path>', 'stage and attach a local file')
1376
+ .option('--upload <id>', 'attach an already-staged upload')
1377
+ .option('--yes', 'apply without prompting')
1378
+ .option('--api-url <url>', 'override the API base URL for this command')
1379
+ .option('--json', 'output raw JSON')
1380
+ .action(async (risk, options) => {
1381
+ await riskAttachCommand({
1382
+ io,
1383
+ risk,
1384
+ file: readStringOption(options, 'file'),
1385
+ uploadId: readStringOption(options, 'upload'),
1386
+ yes: readBooleanOption(options, 'yes'),
1387
+ apiUrl: readStringOption(options, 'apiUrl'),
1388
+ json: readBooleanOption(options, 'json'),
1389
+ });
1390
+ });
1391
+ risk
1392
+ .command('detach <risk> <mappingId>')
1393
+ .description('Detach a file from a risk by its mapping id')
1394
+ .option('--yes', 'apply without prompting')
1395
+ .option('--api-url <url>', 'override the API base URL for this command')
1396
+ .option('--json', 'output raw JSON')
1397
+ .action(async (risk, mappingId, options) => {
1398
+ await riskDetachCommand({
1399
+ io,
1400
+ risk,
1401
+ mappingId,
1402
+ yes: readBooleanOption(options, 'yes'),
1403
+ apiUrl: readStringOption(options, 'apiUrl'),
1404
+ json: readBooleanOption(options, 'json'),
1405
+ });
1406
+ });
1407
+ const check = program.command('check').description('Monitoring checks');
1408
+ check
1409
+ .command('list')
1410
+ .description('List monitoring checks')
1411
+ .option('--integration-type <csv>', 'filter by integration type')
1412
+ .option('--status <csv>', 'filter by status')
1413
+ .option('--page <n>', 'page number')
1414
+ .option('--limit <n>', 'page size')
1415
+ .option('--api-url <url>', 'override the API base URL for this command')
1416
+ .option('--json', 'output raw JSON')
1417
+ .action(async (options) => {
1418
+ await checkListCommand({
1419
+ io,
1420
+ integrationType: readStringOption(options, 'integrationType'),
1421
+ status: readStringOption(options, 'status'),
1422
+ page: readNumberOption(options, 'page'),
1423
+ limit: readNumberOption(options, 'limit'),
1424
+ apiUrl: readStringOption(options, 'apiUrl'),
1425
+ json: readBooleanOption(options, 'json'),
1426
+ });
1427
+ });
1428
+ check
1429
+ .command('resources <check>')
1430
+ .description('List the resources covered by a check')
1431
+ .option('--api-url <url>', 'override the API base URL for this command')
1432
+ .option('--json', 'output raw JSON')
1433
+ .action(async (checkName, options) => {
1434
+ await checkResourcesCommand({
1435
+ io,
1436
+ check: checkName,
1437
+ apiUrl: readStringOption(options, 'apiUrl'),
1438
+ json: readBooleanOption(options, 'json'),
1439
+ });
1440
+ });
1441
+ check
1442
+ .command('run <check>')
1443
+ .description('Show a check\u2019s latest run')
1444
+ .option('--api-url <url>', 'override the API base URL for this command')
1445
+ .option('--json', 'output raw JSON')
1446
+ .action(async (checkName, options) => {
1447
+ await checkRunCommand({
1448
+ io,
1449
+ check: checkName,
1450
+ apiUrl: readStringOption(options, 'apiUrl'),
1451
+ json: readBooleanOption(options, 'json'),
1452
+ });
1453
+ });
1454
+ check
1455
+ .command('integrations')
1456
+ .description('List check integration types')
1457
+ .option('--api-url <url>', 'override the API base URL for this command')
1458
+ .option('--json', 'output raw JSON')
1459
+ .action(async (options) => {
1460
+ await checkIntegrationsCommand({
1461
+ io,
1462
+ apiUrl: readStringOption(options, 'apiUrl'),
1463
+ json: readBooleanOption(options, 'json'),
1464
+ });
1465
+ });
1466
+ check
1467
+ .command('runs <check>')
1468
+ .description('List a check’s run history')
1469
+ .option('--page <n>', 'page number')
1470
+ .option('--limit <n>', 'page size')
1471
+ .option('--api-url <url>', 'override the API base URL for this command')
1472
+ .option('--json', 'output raw JSON')
1473
+ .action(async (name, options) => {
1474
+ await checkRunsCommand({
1475
+ io,
1476
+ check: name,
1477
+ page: readNumberOption(options, 'page'),
1478
+ limit: readNumberOption(options, 'limit'),
1479
+ apiUrl: readStringOption(options, 'apiUrl'),
1480
+ json: readBooleanOption(options, 'json'),
1481
+ });
1482
+ });
1483
+ check
1484
+ .command('findings <check>')
1485
+ .description('List the findings raised from a check')
1486
+ .option('--api-url <url>', 'override the API base URL for this command')
1487
+ .option('--json', 'output raw JSON')
1488
+ .action(async (name, options) => {
1489
+ await checkFindingsCommand({
1490
+ io,
1491
+ check: name,
1492
+ apiUrl: readStringOption(options, 'apiUrl'),
1493
+ json: readBooleanOption(options, 'json'),
1494
+ });
1495
+ });
1496
+ check
1497
+ .command('enable <check>')
1498
+ .description('Enable a check for this tenant')
1499
+ .option('--notes <text>', 'why')
1500
+ .option('--yes', 'apply without prompting')
1501
+ .option('--api-url <url>', 'override the API base URL for this command')
1502
+ .option('--json', 'output raw JSON')
1503
+ .action(async (name, options) => {
1504
+ await checkSetEnabledCommand({
1505
+ io,
1506
+ check: name,
1507
+ enabled: true,
1508
+ notes: readStringOption(options, 'notes'),
1509
+ yes: readBooleanOption(options, 'yes'),
1510
+ apiUrl: readStringOption(options, 'apiUrl'),
1511
+ json: readBooleanOption(options, 'json'),
1512
+ });
1513
+ });
1514
+ check
1515
+ .command('disable <check>')
1516
+ .description('Disable a check for this tenant')
1517
+ .option('--notes <text>', 'why')
1518
+ .option('--yes', 'apply without prompting')
1519
+ .option('--api-url <url>', 'override the API base URL for this command')
1520
+ .option('--json', 'output raw JSON')
1521
+ .action(async (name, options) => {
1522
+ await checkSetEnabledCommand({
1523
+ io,
1524
+ check: name,
1525
+ enabled: false,
1526
+ notes: readStringOption(options, 'notes'),
1527
+ yes: readBooleanOption(options, 'yes'),
1528
+ apiUrl: readStringOption(options, 'apiUrl'),
1529
+ json: readBooleanOption(options, 'json'),
1530
+ });
1531
+ });
1532
+ const job = program.command('job').description('Check background jobs started by other commands');
1533
+ job
1534
+ .command('status <jobId>')
1535
+ .description('Show the status of a background job')
1536
+ .option('--api-url <url>', 'override the API base URL for this command')
1537
+ .option('--json', 'output raw JSON')
1538
+ .action(async (jobId, options) => {
1539
+ await jobStatusCommand({
1540
+ io,
1541
+ jobId,
1542
+ apiUrl: readStringOption(options, 'apiUrl'),
1543
+ json: readBooleanOption(options, 'json'),
1544
+ });
1545
+ });
1546
+ // A bare `ciphrix` shows the banner and help; commands never print the banner.
1547
+ program.action(() => {
1548
+ writeBlock(io.stdout, `${formatBanner(io.theme)}\n\n`);
1549
+ program.outputHelp();
1550
+ });
1551
+ return program;
1552
+ };
1553
+ /**
1554
+ * Runs the CLI and resolves to a process exit code. `--help`/`--version` and
1555
+ * usage errors are handled by commander and mapped to their exit codes.
1556
+ */
1557
+ export const main = async (argv = process.argv, io = createDefaultIO()) => {
1558
+ const program = createProgram(io);
1559
+ program.exitOverride();
1560
+ program.configureOutput({
1561
+ writeOut: (text) => io.stdout.write(text),
1562
+ writeErr: (text) => io.stderr.write(text),
1563
+ });
1564
+ try {
1565
+ await program.parseAsync([...argv]);
1566
+ return 0;
1567
+ }
1568
+ catch (error) {
1569
+ if (error instanceof CommanderError) {
1570
+ return error.exitCode;
1571
+ }
1572
+ const message = error instanceof Error ? error.message : String(error);
1573
+ writeLine(io.stderr, `${io.theme.red('error')} ${message}`);
1574
+ return 1;
1575
+ }
1576
+ finally {
1577
+ setCliInsecureHttpOptIn(false);
1578
+ }
1579
+ };