@cdot65/prisma-airs-cli 5.6.0 → 5.7.1

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 (2) hide show
  1. package/dist/cli/index.js +417 -113
  2. package/package.json +2 -2
package/dist/cli/index.js CHANGED
@@ -522,6 +522,10 @@ ${INDENT}${chalk3.bold(label)}
522
522
  warn(msg) {
523
523
  console.log(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
524
524
  },
525
+ /** Safety warnings remain visible in quiet mode without corrupting structured stdout. */
526
+ warning(msg) {
527
+ console.error(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
528
+ },
525
529
  /** Neutral informational callout. Quiet-suppressed. */
526
530
  info(msg) {
527
531
  if (quietMode) return;
@@ -8800,6 +8804,11 @@ var SdkDictionariesService = class {
8800
8804
  }
8801
8805
  };
8802
8806
 
8807
+ // src/cli/commands/dlp/config.ts
8808
+ async function loadDlpClientOptions() {
8809
+ return managementClientOptions(await loadConfig());
8810
+ }
8811
+
8803
8812
  // src/cli/commands/dlp/patch.ts
8804
8813
  import { readFile as readFile7 } from "fs/promises";
8805
8814
  function buildMergePatch(opts) {
@@ -8890,7 +8899,7 @@ function register(dlp) {
8890
8899
  try {
8891
8900
  const { page, size } = resolvePageParams(listCmd, opts);
8892
8901
  const includeKeywords = opts.keywords || opts.includeKeywords;
8893
- const svc = new SdkDictionariesService();
8902
+ const svc = new SdkDictionariesService(await loadDlpClientOptions());
8894
8903
  const params = {
8895
8904
  size,
8896
8905
  sort: opts.sort,
@@ -8905,17 +8914,18 @@ function register(dlp) {
8905
8914
  fail(err);
8906
8915
  }
8907
8916
  });
8908
- group.command("create").description("Create dictionary via multipart upload").option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "Keyword file").option("--metadata-file <path>", "JSON metadata file (overrides --name/--category/...)").option("--include-keywords", "Include keywords in response").option("--output <fmt>", "Output format", "pretty").action(async (opts) => {
8917
+ group.command("create").description("Create dictionary via multipart upload").option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "Keyword file").option("--metadata-file <path>", "JSON metadata file (overrides --name/--category/...)").option("--include-keywords", "Include keywords in response").option("--output <fmt>", "Output format", "pretty").action(async (opts, command) => {
8909
8918
  try {
8919
+ const format = await resolveOutput(command, opts);
8910
8920
  const metadata = await buildMetadata(opts);
8911
8921
  if (!opts.file) throw new Error("--file is required (multipart upload)");
8912
8922
  const file = await readFile8(opts.file);
8913
- const r = await new SdkDictionariesService().create({
8923
+ const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
8914
8924
  metadata,
8915
8925
  file,
8916
8926
  includeKeywords: opts.includeKeywords
8917
8927
  });
8918
- dlpDictionaries.renderCreated(r, opts.output);
8928
+ dlpDictionaries.renderCreated(r, format);
8919
8929
  } catch (err) {
8920
8930
  usageError(err instanceof Error ? err.message : String(err));
8921
8931
  }
@@ -8924,7 +8934,9 @@ function register(dlp) {
8924
8934
  try {
8925
8935
  const includeKeywords = opts.keywords || opts.includeKeywords;
8926
8936
  dlpDictionaries.renderGet(
8927
- await new SdkDictionariesService().get(id, { includeKeywords }),
8937
+ await new SdkDictionariesService(await loadDlpClientOptions()).get(id, {
8938
+ includeKeywords
8939
+ }),
8928
8940
  await resolveOutput(getCmd, opts)
8929
8941
  );
8930
8942
  } catch (err) {
@@ -8933,12 +8945,13 @@ function register(dlp) {
8933
8945
  });
8934
8946
  group.command("replace <id>").description(
8935
8947
  'Full-replace via multipart upload. --file required. May return 200 (body) or 204 (re-get; falls back to "(state not echoed)" on transient failure).'
8936
- ).option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "Keyword file (required)").option("--metadata-file <path>", "JSON metadata file").option("--include-keywords", "").option("--output <fmt>", "Output format", "pretty").action(async (id, opts) => {
8948
+ ).option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "Keyword file (required)").option("--metadata-file <path>", "JSON metadata file").option("--include-keywords", "").option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
8937
8949
  try {
8938
8950
  const metadata = await buildMetadata(opts);
8951
+ const format = await resolveOutput(command, opts);
8939
8952
  if (!opts.file) throw new Error("--file is required (multipart upload)");
8940
8953
  const file = await readFile8(opts.file);
8941
- const r = await new SdkDictionariesService().replace(id, {
8954
+ const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
8942
8955
  metadata,
8943
8956
  file,
8944
8957
  includeKeywords: opts.includeKeywords
@@ -8946,22 +8959,23 @@ function register(dlp) {
8946
8959
  if ("kind" in r && r.kind === "fallback") {
8947
8960
  dlpDictionaries.renderReplaced204Fallback(id);
8948
8961
  } else {
8949
- dlpDictionaries.renderReplaced(r, opts.output);
8962
+ dlpDictionaries.renderReplaced(r, format);
8950
8963
  }
8951
8964
  } catch (err) {
8952
8965
  usageError(err instanceof Error ? err.message : String(err));
8953
8966
  }
8954
8967
  });
8955
- group.command("patch <id>").option("--body-file <path>", "").option("--set <k=v...>", "(repeatable)", (v, p = []) => [...p, v]).option("--clear <key...>", "(repeatable)", (v, p = []) => [...p, v]).option("--output <fmt>", "Output format", "pretty").action(async (id, opts) => {
8968
+ group.command("patch <id>").option("--body-file <path>", "").option("--set <k=v...>", "(repeatable)", (v, p = []) => [...p, v]).option("--clear <key...>", "(repeatable)", (v, p = []) => [...p, v]).option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
8956
8969
  try {
8970
+ const format = await resolveOutput(command, opts);
8957
8971
  if (opts.bodyFile && (opts.set || opts.clear)) {
8958
8972
  throw new Error("--body-file is mutually exclusive with --set/--clear");
8959
8973
  }
8960
8974
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
8961
8975
  dlpDictionaries.renderPatched(
8962
8976
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
8963
- await new SdkDictionariesService().patch(id, body),
8964
- opts.output
8977
+ await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
8978
+ format
8965
8979
  );
8966
8980
  } catch (err) {
8967
8981
  usageError(err instanceof Error ? err.message : String(err));
@@ -8969,7 +8983,7 @@ function register(dlp) {
8969
8983
  });
8970
8984
  group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
8971
8985
  try {
8972
- await new SdkDictionariesService().delete(id);
8986
+ await new SdkDictionariesService(await loadDlpClientOptions()).delete(id);
8973
8987
  dlpDictionaries.renderDeleted(id);
8974
8988
  } catch (err) {
8975
8989
  fail(err);
@@ -9142,7 +9156,7 @@ function register2(dlp) {
9142
9156
  listCmd.action(async (opts) => {
9143
9157
  try {
9144
9158
  const { page, size } = resolvePageParams(listCmd, opts);
9145
- const svc = new SdkDataFilteringProfilesService();
9159
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9146
9160
  const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9147
9161
  const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort });
9148
9162
  dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
@@ -9152,20 +9166,21 @@ function register2(dlp) {
9152
9166
  });
9153
9167
  const getCmd = group.command("get <id>").description("Get a filtering profile by id").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (id, opts) => {
9154
9168
  try {
9155
- const svc = new SdkDataFilteringProfilesService();
9169
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9156
9170
  dlpFilteringProfiles.renderGet(await svc.get(id), await resolveOutput(getCmd, opts));
9157
9171
  } catch (err) {
9158
9172
  fail(err);
9159
9173
  }
9160
9174
  });
9161
- group.command("replace <id>").description("Full-replace a filtering profile (PUT)").option("--file-based", "Apply to file-based scans (boolean)").option("--non-file-based", "Apply to non-file-based scans (boolean)").option("--description <s>", "Description").option("--direction <s>", "Direction: BOTH|UPLOAD|DOWNLOAD").option("--log-severity <s>", "Severity: CRITICAL|HIGH|MEDIUM|LOW|INFORMATIONAL").option("--scan-type <s>", "Scan type: include|exclude").option("--data-profile-id <n>", "Data profile ID", (v) => Number(v)).option("--euc-template-id <s>", "EUC template ID").option("--end-user-coaching", "Enable end-user coaching").option("--granular", "Granular profile").option("--file-type <s>", "File type (repeatable)", repeatable).option("--body <json|->", 'Raw JSON body (escape hatch; or "-" for stdin)').option("--body-file <path>", "Raw JSON body file (escape hatch)").option("--output <fmt>", "Output format", "pretty").action(async (id, opts) => {
9175
+ group.command("replace <id>").description("Full-replace a filtering profile (PUT)").option("--file-based", "Apply to file-based scans (boolean)").option("--non-file-based", "Apply to non-file-based scans (boolean)").option("--description <s>", "Description").option("--direction <s>", "Direction: BOTH|UPLOAD|DOWNLOAD").option("--log-severity <s>", "Severity: CRITICAL|HIGH|MEDIUM|LOW|INFORMATIONAL").option("--scan-type <s>", "Scan type: include|exclude").option("--data-profile-id <n>", "Data profile ID", (v) => Number(v)).option("--euc-template-id <s>", "EUC template ID").option("--end-user-coaching", "Enable end-user coaching").option("--granular", "Granular profile").option("--file-type <s>", "File type (repeatable)", repeatable).option("--body <json|->", 'Raw JSON body (escape hatch; or "-" for stdin)').option("--body-file <path>", "Raw JSON body file (escape hatch)").option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
9162
9176
  try {
9163
9177
  const body = await resolveReplaceBody(opts);
9164
- const svc = new SdkDataFilteringProfilesService();
9178
+ const format = await resolveOutput(command, opts);
9179
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9165
9180
  dlpFilteringProfiles.renderReplaced(
9166
9181
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9167
9182
  await svc.replace(id, body),
9168
- opts.output
9183
+ format
9169
9184
  );
9170
9185
  } catch (err) {
9171
9186
  usageError(err instanceof Error ? err.message : String(err));
@@ -9310,7 +9325,7 @@ function register4(dlp) {
9310
9325
  listCmd.action(async (opts) => {
9311
9326
  try {
9312
9327
  const { page, size } = resolvePageParams(listCmd, opts);
9313
- const svc = new SdkDataPatternsService();
9328
+ const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9314
9329
  const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9315
9330
  dlpPatterns.renderList(
9316
9331
  result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
@@ -9320,22 +9335,25 @@ function register4(dlp) {
9320
9335
  fail(err);
9321
9336
  }
9322
9337
  });
9323
- writeFlags(group.command("create").description("Create a data pattern")).action(async (opts) => {
9324
- try {
9325
- const body = await resolveWriteBody(opts);
9326
- dlpPatterns.renderCreated(
9327
- // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9328
- await new SdkDataPatternsService().create(body),
9329
- opts.output
9330
- );
9331
- } catch (err) {
9332
- usageError(err instanceof Error ? err.message : String(err));
9338
+ writeFlags(group.command("create").description("Create a data pattern")).action(
9339
+ async (opts, command) => {
9340
+ try {
9341
+ const format = await resolveOutput(command, opts);
9342
+ const body = await resolveWriteBody(opts);
9343
+ dlpPatterns.renderCreated(
9344
+ // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9345
+ await new SdkDataPatternsService(await loadDlpClientOptions()).create(body),
9346
+ format
9347
+ );
9348
+ } catch (err) {
9349
+ usageError(err instanceof Error ? err.message : String(err));
9350
+ }
9333
9351
  }
9334
- });
9352
+ );
9335
9353
  const getCmd = group.command("get <id>").description("Get a data pattern by id").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (id, opts) => {
9336
9354
  try {
9337
9355
  dlpPatterns.renderGet(
9338
- await new SdkDataPatternsService().get(id),
9356
+ await new SdkDataPatternsService(await loadDlpClientOptions()).get(id),
9339
9357
  await resolveOutput(getCmd, opts)
9340
9358
  );
9341
9359
  } catch (err) {
@@ -9343,13 +9361,14 @@ function register4(dlp) {
9343
9361
  }
9344
9362
  });
9345
9363
  writeFlags(group.command("replace <id>").description("Full-replace a data pattern (PUT)")).action(
9346
- async (id, opts) => {
9364
+ async (id, opts, command) => {
9347
9365
  try {
9366
+ const format = await resolveOutput(command, opts);
9348
9367
  const body = await resolveWriteBody(opts);
9349
9368
  dlpPatterns.renderReplaced(
9350
9369
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9351
- await new SdkDataPatternsService().replace(id, body),
9352
- opts.output
9370
+ await new SdkDataPatternsService(await loadDlpClientOptions()).replace(id, body),
9371
+ format
9353
9372
  );
9354
9373
  } catch (err) {
9355
9374
  usageError(err instanceof Error ? err.message : String(err));
@@ -9358,16 +9377,17 @@ function register4(dlp) {
9358
9377
  );
9359
9378
  group.command("patch <id>").description(
9360
9379
  `JSON Merge Patch. Use --body-file for nested fields. --set/--clear coerce values: numbers/booleans/JSON literals. To force a string, quote: --set count='"5"'.`
9361
- ).option("--body-file <path>", "JSON merge-patch body file").option("--set <k=v...>", "Set scalar field (repeatable)", repeatable).option("--clear <key...>", "Clear field via merge-patch null (repeatable)", repeatable).option("--output <fmt>", "Output format", "pretty").action(async (id, opts) => {
9380
+ ).option("--body-file <path>", "JSON merge-patch body file").option("--set <k=v...>", "Set scalar field (repeatable)", repeatable).option("--clear <key...>", "Clear field via merge-patch null (repeatable)", repeatable).option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
9362
9381
  try {
9382
+ const format = await resolveOutput(command, opts);
9363
9383
  if (opts.bodyFile && (opts.set || opts.clear)) {
9364
9384
  throw new Error("--body-file is mutually exclusive with --set/--clear");
9365
9385
  }
9366
9386
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9367
9387
  dlpPatterns.renderPatched(
9368
9388
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9369
- await new SdkDataPatternsService().patch(id, body),
9370
- opts.output
9389
+ await new SdkDataPatternsService(await loadDlpClientOptions()).patch(id, body),
9390
+ format
9371
9391
  );
9372
9392
  } catch (err) {
9373
9393
  usageError(err instanceof Error ? err.message : String(err));
@@ -9375,7 +9395,7 @@ function register4(dlp) {
9375
9395
  });
9376
9396
  group.command("delete <id>").description("Soft-delete (archive) a data pattern").action(async (id) => {
9377
9397
  try {
9378
- await new SdkDataPatternsService().delete(id);
9398
+ await new SdkDataPatternsService(await loadDlpClientOptions()).delete(id);
9379
9399
  dlpPatterns.renderArchived(id);
9380
9400
  } catch (err) {
9381
9401
  fail(err);
@@ -9451,7 +9471,7 @@ function register5(dlp) {
9451
9471
  listCmd.action(async (opts) => {
9452
9472
  try {
9453
9473
  const { page, size } = resolvePageParams(listCmd, opts);
9454
- const svc = new SdkDataProfilesService();
9474
+ const svc = new SdkDataProfilesService(await loadDlpClientOptions());
9455
9475
  const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9456
9476
  dlpProfiles.renderList(
9457
9477
  result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
@@ -9461,22 +9481,25 @@ function register5(dlp) {
9461
9481
  fail(err);
9462
9482
  }
9463
9483
  });
9464
- writeFlags2(group.command("create").description("Create a data profile")).action(async (opts) => {
9465
- try {
9466
- const body = await resolveWriteBody2(opts);
9467
- dlpProfiles.renderCreated(
9468
- // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9469
- await new SdkDataProfilesService().create(body),
9470
- opts.output
9471
- );
9472
- } catch (err) {
9473
- usageError(err instanceof Error ? err.message : String(err));
9484
+ writeFlags2(group.command("create").description("Create a data profile")).action(
9485
+ async (opts, command) => {
9486
+ try {
9487
+ const format = await resolveOutput(command, opts);
9488
+ const body = await resolveWriteBody2(opts);
9489
+ dlpProfiles.renderCreated(
9490
+ // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9491
+ await new SdkDataProfilesService(await loadDlpClientOptions()).create(body),
9492
+ format
9493
+ );
9494
+ } catch (err) {
9495
+ usageError(err instanceof Error ? err.message : String(err));
9496
+ }
9474
9497
  }
9475
- });
9498
+ );
9476
9499
  const getCmd = group.command("get <id>").description("Get a data profile by id").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (id, opts) => {
9477
9500
  try {
9478
9501
  dlpProfiles.renderGet(
9479
- await new SdkDataProfilesService().get(id),
9502
+ await new SdkDataProfilesService(await loadDlpClientOptions()).get(id),
9480
9503
  await resolveOutput(getCmd, opts)
9481
9504
  );
9482
9505
  } catch (err) {
@@ -9484,13 +9507,14 @@ function register5(dlp) {
9484
9507
  }
9485
9508
  });
9486
9509
  writeFlags2(group.command("replace <id>").description("Full-replace a data profile (PUT)")).action(
9487
- async (id, opts) => {
9510
+ async (id, opts, command) => {
9488
9511
  try {
9512
+ const format = await resolveOutput(command, opts);
9489
9513
  const body = await resolveWriteBody2(opts);
9490
9514
  dlpProfiles.renderReplaced(
9491
9515
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9492
- await new SdkDataProfilesService().replace(id, body),
9493
- opts.output
9516
+ await new SdkDataProfilesService(await loadDlpClientOptions()).replace(id, body),
9517
+ format
9494
9518
  );
9495
9519
  } catch (err) {
9496
9520
  usageError(err instanceof Error ? err.message : String(err));
@@ -9499,16 +9523,17 @@ function register5(dlp) {
9499
9523
  );
9500
9524
  group.command("patch <id>").description(
9501
9525
  `JSON Merge Patch. body must include name + profile_type. Use --body-file for nested fields. --set/--clear coerce values: numbers/booleans/JSON literals. To force a string, quote: --set count='"5"'.`
9502
- ).option("--body-file <path>", "JSON merge-patch body file").option("--set <k=v...>", "Set scalar field (repeatable)", repeatable).option("--clear <key...>", "Clear field via merge-patch null (repeatable)", repeatable).option("--output <fmt>", "Output format", "pretty").action(async (id, opts) => {
9526
+ ).option("--body-file <path>", "JSON merge-patch body file").option("--set <k=v...>", "Set scalar field (repeatable)", repeatable).option("--clear <key...>", "Clear field via merge-patch null (repeatable)", repeatable).option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
9503
9527
  try {
9528
+ const format = await resolveOutput(command, opts);
9504
9529
  if (opts.bodyFile && (opts.set || opts.clear)) {
9505
9530
  throw new Error("--body-file is mutually exclusive with --set/--clear");
9506
9531
  }
9507
9532
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9508
9533
  dlpProfiles.renderPatched(
9509
9534
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9510
- await new SdkDataProfilesService().patch(id, body),
9511
- opts.output
9535
+ await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
9536
+ format
9512
9537
  );
9513
9538
  } catch (err) {
9514
9539
  usageError(err instanceof Error ? err.message : String(err));
@@ -9551,6 +9576,102 @@ import {
9551
9576
  SecurityProfileSchema
9552
9577
  } from "@cdot65/prisma-airs-sdk";
9553
9578
  import { z as z5 } from "zod";
9579
+
9580
+ // src/backup/runtime-policy.ts
9581
+ function object(value) {
9582
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9583
+ }
9584
+ function compareRuntimePolicies(expected, actual) {
9585
+ const serverDefaults = [];
9586
+ const differences = [];
9587
+ const visit = (source, target, path3, insideToxicCategories = false, insideTopicGuardrails = false) => {
9588
+ if (source === target) return;
9589
+ if (Array.isArray(source) && Array.isArray(target)) {
9590
+ if (source.length !== target.length) differences.push(`${path3}.length`);
9591
+ source.forEach((item, index) => {
9592
+ visit(
9593
+ item,
9594
+ target[index],
9595
+ `${path3}[${index}]`,
9596
+ insideToxicCategories,
9597
+ insideTopicGuardrails
9598
+ );
9599
+ });
9600
+ return;
9601
+ }
9602
+ if (object(source) && object(target)) {
9603
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
9604
+ const field = `${path3}.${key}`;
9605
+ if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
9606
+ const model = /^policy\.ai-security-profiles\[\d+\]\.model-configuration/;
9607
+ const relative = path3.replace(model, "model");
9608
+ let allowed;
9609
+ if (relative === "model.data-protection" && key === "database-security") allowed = null;
9610
+ if (key === "severity") {
9611
+ if (/^model\.data-protection\.database-security\[\d+\]$/.test(relative)) {
9612
+ const values = {
9613
+ "database-security-create": "medium",
9614
+ "database-security-read": "low",
9615
+ "database-security-update": "medium",
9616
+ "database-security-delete": "high"
9617
+ };
9618
+ if (source.name === target.name && typeof source.name === "string")
9619
+ allowed = Object.hasOwn(values, source.name) ? values[source.name] : void 0;
9620
+ }
9621
+ if (relative === "model.data-protection.source-code-detection") allowed = "high";
9622
+ if (relative === "model.app-protection.malicious-code-protection" && source.name === "malicious-code" && target.name === source.name)
9623
+ allowed = "high";
9624
+ if (/^model\.model-protection\[\d+\]$/.test(relative) && (source.name === "prompt-injection" || source.name === "topic-guardrails" || source.name === "contextual-grounding" && source.action === "block") && target.name === source.name)
9625
+ allowed = "medium";
9626
+ if (insideTopicGuardrails && /^model\.model-protection\[\d+\]\.topic-list\[\d+\]\.topic\[\d+\]$/.test(relative) && typeof source.topic_id === "string" && source.topic_id.length > 0 && source.topic_id === target.topic_id && source.topic_name === target.topic_name && Number.isInteger(source.revision) && source.revision === target.revision)
9627
+ allowed = "medium";
9628
+ if (/^model\.agent-protection\[\d+\]$/.test(relative) && source.name === "agent-security" && target.name === source.name)
9629
+ allowed = "medium";
9630
+ }
9631
+ if (relative === "model.app-protection" && key === "url-detected-severity")
9632
+ allowed = "low";
9633
+ if (relative === "model.app-protection" && key === "default-url-category" && object(target[key])) {
9634
+ const value = target[key];
9635
+ if (Object.keys(value).length === 1 && value.member === null) {
9636
+ serverDefaults.push(field);
9637
+ continue;
9638
+ }
9639
+ }
9640
+ if ((/^model\.model-protection\[\d+\]$/.test(relative) && source.name === "toxic-content" && target.name === source.name || insideToxicCategories && /^model\.model-protection\[\d+\]\.toxic-category-list\[\d+\]$/.test(relative) && typeof source.category === "string" && source.category.length > 0 && source.category === target.category) && key === "severity-by-confidence" && object(target[key])) {
9641
+ const value = target[key];
9642
+ if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
9643
+ serverDefaults.push(field);
9644
+ continue;
9645
+ }
9646
+ }
9647
+ if (allowed !== void 0 && target[key] === allowed) {
9648
+ serverDefaults.push(field);
9649
+ continue;
9650
+ }
9651
+ differences.push(field);
9652
+ } else if (!Object.hasOwn(target, key)) differences.push(field);
9653
+ else
9654
+ visit(
9655
+ source[key],
9656
+ target[key],
9657
+ field,
9658
+ key === "toxic-category-list" && source.name === "toxic-content" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
9659
+ path3
9660
+ ),
9661
+ key === "topic-list" && source.name === "topic-guardrails" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
9662
+ path3
9663
+ ) || insideTopicGuardrails && key === "topic" && source.action === "block" && target.action === "block"
9664
+ );
9665
+ }
9666
+ return;
9667
+ }
9668
+ differences.push(path3);
9669
+ };
9670
+ visit(expected, actual, "policy");
9671
+ return { matches: differences.length === 0, serverDefaults, differences };
9672
+ }
9673
+
9674
+ // src/backup/runtime-profiles.ts
9554
9675
  var RuntimeProfilesBackupSchema = z5.object({
9555
9676
  version: z5.literal(1),
9556
9677
  resourceType: z5.literal("runtime-security-profiles"),
@@ -9559,7 +9680,7 @@ var RuntimeProfilesBackupSchema = z5.object({
9559
9680
  profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
9560
9681
  topics: z5.array(CustomTopicSchema).max(1e4)
9561
9682
  }).strict();
9562
- function object(value) {
9683
+ function object2(value) {
9563
9684
  return value !== null && typeof value === "object" && !Array.isArray(value);
9564
9685
  }
9565
9686
  function pagesLimit(value) {
@@ -9626,7 +9747,7 @@ function topicReferences(policy) {
9626
9747
  value.forEach(visit);
9627
9748
  return;
9628
9749
  }
9629
- if (!object(value)) return;
9750
+ if (!object2(value)) return;
9630
9751
  if ("topic_id" in value) {
9631
9752
  if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
9632
9753
  throw new Error("Invalid topic identity in profile policy");
@@ -9679,26 +9800,48 @@ function sameTopic(a, b) {
9679
9800
  return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
9680
9801
  }
9681
9802
  function dlpReferences(policy) {
9682
- if (!object(policy)) return [];
9803
+ if (!object2(policy)) return [];
9683
9804
  const refs = [];
9684
9805
  for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
9685
- if (!object(entry) || typeof entry.name !== "string")
9806
+ if (!object2(entry) || typeof entry.name !== "string")
9686
9807
  throw new Error("Invalid embedded DLP reference");
9687
9808
  refs.push({ value: entry, name: entry.name, kind: "embedded" });
9688
9809
  }
9689
9810
  for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
9690
- if (!object(entry) || !object(entry["model-configuration"])) continue;
9811
+ if (!object2(entry) || !object2(entry["model-configuration"])) continue;
9691
9812
  const protection = entry["model-configuration"]["data-protection"];
9692
- if (!object(protection) || !object(protection["data-leak-detection"])) continue;
9813
+ if (!object2(protection) || !object2(protection["data-leak-detection"])) continue;
9693
9814
  const members = protection["data-leak-detection"].member;
9694
9815
  if (!Array.isArray(members)) continue;
9695
9816
  for (const member of members) {
9696
- if (!object(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
9817
+ if (!object2(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
9818
+ if (member.text.toLowerCase() === "sensitive content" && (member.id === "" || member.id === void 0) && (member.version === "2" || member.version === void 0) && !refs.some((ref) => ref.kind === "embedded" && ref.name === member.text))
9819
+ continue;
9697
9820
  refs.push({ value: member, name: member.text, kind: "member" });
9698
9821
  }
9699
9822
  }
9700
9823
  return refs;
9701
9824
  }
9825
+ function basicFallback(policy) {
9826
+ const result = structuredClone(policy);
9827
+ const customNames = new Set(dlpReferences(result).map((ref) => ref.name));
9828
+ let converted = false;
9829
+ for (const entry of result?.["ai-security-profiles"] ?? []) {
9830
+ const detection = entry["model-configuration"]?.["data-protection"]?.["data-leak-detection"];
9831
+ if (!detection?.member?.some((member) => customNames.has(member.text))) continue;
9832
+ if (!["block", "allow", ""].includes(detection.action))
9833
+ throw new Error("Cannot safely fall back to Basic: unsupported DLP action");
9834
+ if (detection["mask-data-inline"] && detection.action !== "block")
9835
+ throw new Error("Cannot safely fall back to Basic: inline masking requires a block action");
9836
+ detection.member = detection.action === "" ? null : [{ text: "sensitive content", id: "", version: "2" }];
9837
+ converted = true;
9838
+ }
9839
+ if (!converted)
9840
+ throw new Error("Cannot safely fall back to Basic: no matching DLP detection configuration");
9841
+ if (result && "dlp-data-profiles" in result) result["dlp-data-profiles"] = [];
9842
+ PolicySchema.parse(result);
9843
+ return result;
9844
+ }
9702
9845
  function assertKnownPolicyReferences(policy) {
9703
9846
  const known = new Map([
9704
9847
  ...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
@@ -9714,7 +9857,7 @@ function assertKnownPolicyReferences(policy) {
9714
9857
  });
9715
9858
  return;
9716
9859
  }
9717
- if (!object(value)) return;
9860
+ if (!object2(value)) return;
9718
9861
  for (const [key, child] of Object.entries(value)) {
9719
9862
  if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
9720
9863
  throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
@@ -9744,7 +9887,10 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9744
9887
  if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
9745
9888
  throw new Error("Invalid --name-prefix");
9746
9889
  const conflict = options.onConflict ?? "error";
9747
- if (!["error", "skip", "update"].includes(conflict)) throw new Error("Invalid conflict policy");
9890
+ if (!["error", "skip", "update", "verify"].includes(conflict))
9891
+ throw new Error("Invalid conflict policy");
9892
+ const missingDlp = options.onMissingDlp ?? "error";
9893
+ if (!["error", "basic"].includes(missingDlp)) throw new Error("Invalid missing DLP policy");
9748
9894
  const selected = latestProfiles(backup.profiles);
9749
9895
  assertTenant(selected, backup.source.tsgId);
9750
9896
  if (selected.length !== backup.profiles.length)
@@ -9766,14 +9912,14 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9766
9912
  const match = existing.find((p) => p.profile_name === name);
9767
9913
  if (match && conflict === "error")
9768
9914
  throw new Error(
9769
- `Destination profile already exists: ${name}; choose --on-conflict skip or update`
9915
+ `Destination profile already exists: ${name}; choose --on-conflict verify, skip or update`
9770
9916
  );
9771
9917
  if (match && !match.profile_id) throw new Error("Destination profile has no identity");
9772
9918
  return {
9773
9919
  source: structuredClone(source),
9774
9920
  name,
9775
9921
  existing: match,
9776
- action: match ? conflict === "skip" ? "skip" : "update" : "create"
9922
+ action: match ? conflict === "skip" ? "skip" : conflict === "verify" ? "verify" : "update" : "create"
9777
9923
  };
9778
9924
  });
9779
9925
  const changing = profilePlans.filter((p) => p.action !== "skip");
@@ -9807,46 +9953,102 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9807
9953
  );
9808
9954
  topicPlans.set(key, { key, name, source, existing: match });
9809
9955
  }
9810
- const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
9956
+ const dlpRefs = crossTenant || missingDlp === "basic" ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
9811
9957
  const names = [...new Set(dlpRefs.map((ref) => ref.name))];
9812
- for (const name of names)
9813
- if (!options.dlpMap?.[name])
9958
+ const targetFor = (name) => Object.hasOwn(options.dlpMap ?? {}, name) ? options.dlpMap?.[name] : crossTenant ? void 0 : name;
9959
+ const unresolved = /* @__PURE__ */ new Set();
9960
+ for (const name of names) {
9961
+ if (!targetFor(name) && missingDlp === "error")
9814
9962
  throw new Error(
9815
- `Cross-tenant DLP reference requires explicit --dlp-map ${name}=<destination-name>; DLP resources are not cloned`
9963
+ `Cross-tenant DLP reference requires explicit --dlp-map "${name}=<destination-name>"; DLP resources are not cloned. Inspect destination resources with airs runtime dlp profiles list --all --output json, or explicitly accept --on-missing-dlp basic.`
9816
9964
  );
9817
- const catalog = names.length ? await dataProfiles(api, maxPages2) : [];
9818
- const dlpMappings = names.map((name) => {
9819
- const target = options.dlpMap?.[name];
9820
- const matches = catalog.filter((p) => p.name === target && p.profile_status !== "deleted");
9965
+ if (!targetFor(name)) unresolved.add(name);
9966
+ }
9967
+ const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages2) : [];
9968
+ const resolved = [];
9969
+ for (const name of names) {
9970
+ const target = targetFor(name);
9971
+ if (!target) continue;
9972
+ const matches = catalog.filter(
9973
+ (p) => p.name === target && p.profile_status !== "deleted" && p.profile_status !== "disabled"
9974
+ );
9975
+ if (!matches.length && missingDlp === "basic") {
9976
+ unresolved.add(name);
9977
+ continue;
9978
+ }
9821
9979
  if (matches.length !== 1 || !matches[0].id)
9822
9980
  throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
9823
9981
  if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
9824
9982
  throw new Error(`Destination DLP version is unknown: ${target}`);
9825
- return {
9983
+ resolved.push({
9826
9984
  source: name,
9827
9985
  destination: String(target),
9828
9986
  id: matches[0].id,
9829
9987
  version: matches[0].version ?? void 0
9830
- };
9831
- });
9832
- return {
9988
+ });
9989
+ }
9990
+ const dlpFallbacks = [];
9991
+ for (const item of changing) {
9992
+ const replaced = [...new Set(dlpReferences(item.source.policy).map((ref) => ref.name))];
9993
+ const missing = replaced.filter((name) => unresolved.has(name));
9994
+ if (!missing.length) continue;
9995
+ item.fallbackPolicy = basicFallback(item.source.policy);
9996
+ dlpFallbacks.push({
9997
+ profile: item.name,
9998
+ unresolved: missing,
9999
+ replaced,
10000
+ reason: "unmapped-or-missing",
10001
+ protection: "basic"
10002
+ });
10003
+ }
10004
+ const remaining = new Set(
10005
+ changing.flatMap(
10006
+ (p) => dlpReferences(p.fallbackPolicy ?? p.source.policy).map((ref) => ref.name)
10007
+ )
10008
+ );
10009
+ const dlpMappings = resolved.filter((mapping) => remaining.has(mapping.source));
10010
+ const plan = {
9833
10011
  sourceTsgId: backup.source.tsgId,
9834
10012
  destinationTsgId,
9835
10013
  profiles: profilePlans,
9836
10014
  topics: [...topicPlans.values()],
9837
10015
  dlpMappings,
10016
+ dlpFallbacks,
10017
+ serverDefaults: [],
9838
10018
  maxPages: maxPages2
9839
10019
  };
10020
+ const existingTopics = new Map(
10021
+ plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
10022
+ );
10023
+ for (const item of profilePlans.filter((p) => p.action === "verify")) {
10024
+ const expected = mappedPolicy(item, plan, existingTopics);
10025
+ const comparison = compareRuntimePolicies(expected, item.existing?.policy);
10026
+ if (!comparison.matches || item.source.active !== void 0 && item.existing?.active !== item.source.active)
10027
+ throw new Error(
10028
+ `Destination profile does not match restore plan: ${item.name}; verification made no changes`
10029
+ );
10030
+ if (comparison.serverDefaults.length)
10031
+ plan.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
10032
+ }
10033
+ return plan;
9840
10034
  }
9841
10035
  function canonical(value) {
9842
- const order = (v) => Array.isArray(v) ? v.map(order) : object(v) ? Object.fromEntries(
10036
+ const order = (v) => Array.isArray(v) ? v.map(order) : object2(v) ? Object.fromEntries(
9843
10037
  Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
9844
10038
  ) : v;
9845
10039
  return JSON.stringify(order(value));
9846
10040
  }
10041
+ function canonicalPolicy(policy) {
10042
+ const value = structuredClone(policy);
10043
+ for (const entry of value?.["ai-security-profiles"] ?? []) {
10044
+ const protection = entry["model-configuration"]?.["data-protection"];
10045
+ if (protection && !("database-security" in protection)) protection["database-security"] = null;
10046
+ }
10047
+ return canonical(value);
10048
+ }
9847
10049
  function unchanged(current, prior) {
9848
10050
  return Boolean(
9849
- current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonical(current.policy) === canonical(prior.policy)
10051
+ current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonicalPolicy(current.policy) === canonicalPolicy(prior.policy)
9850
10052
  );
9851
10053
  }
9852
10054
  async function restoreRuntimeProfiles(api, plan) {
@@ -9854,6 +10056,8 @@ async function restoreRuntimeProfiles(api, plan) {
9854
10056
  sourceTsgId: plan.sourceTsgId,
9855
10057
  destinationTsgId: plan.destinationTsgId,
9856
10058
  complete: false,
10059
+ dlpFallbacks: structuredClone(plan.dlpFallbacks ?? []),
10060
+ serverDefaults: [],
9857
10061
  topics: [],
9858
10062
  profiles: []
9859
10063
  };
@@ -9864,7 +10068,7 @@ async function restoreRuntimeProfiles(api, plan) {
9864
10068
  const catalog = await dataProfiles(api, plan.maxPages);
9865
10069
  for (const mapping of plan.dlpMappings) {
9866
10070
  const matches = catalog.filter(
9867
- (p) => p.name === mapping.destination && p.profile_status !== "deleted"
10071
+ (p) => p.name === mapping.destination && p.profile_status !== "deleted" && p.profile_status !== "disabled"
9868
10072
  );
9869
10073
  if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
9870
10074
  throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
@@ -9910,27 +10114,7 @@ async function restoreRuntimeProfiles(api, plan) {
9910
10114
  result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
9911
10115
  continue;
9912
10116
  }
9913
- const policy = structuredClone(item.source.policy);
9914
- for (const ref of topicReferences(policy)) {
9915
- const topic = mappedTopics.get(topicKey(ref));
9916
- if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
9917
- ref.topic_id = topic.topic_id;
9918
- ref.topic_name = topic.topic_name;
9919
- ref.revision = topic.revision;
9920
- }
9921
- for (const ref of dlpReferences(policy)) {
9922
- const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
9923
- if (!mapping) continue;
9924
- if (ref.kind === "embedded") {
9925
- ref.value.name = mapping.destination;
9926
- ref.value.uuid = mapping.id;
9927
- } else ref.value.text = mapping.destination;
9928
- if ("id" in ref.value) ref.value.id = mapping.id;
9929
- if ("version" in ref.value) {
9930
- if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
9931
- ref.value.version = String(mapping.version);
9932
- }
9933
- }
10117
+ const policy = mappedPolicy(item, plan, mappedTopics);
9934
10118
  const body = CreateSecurityProfileRequestSchema.parse({
9935
10119
  profile_name: item.name,
9936
10120
  active: item.source.active,
@@ -9941,6 +10125,14 @@ async function restoreRuntimeProfiles(api, plan) {
9941
10125
  const current = inventory2.find((p) => p.profile_name === item.name);
9942
10126
  if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
9943
10127
  throw new Error(`Destination profile changed after planning: ${item.name}`);
10128
+ if (item.action === "verify") {
10129
+ const comparison2 = compareRuntimePolicies(policy, current?.policy);
10130
+ if (!comparison2.matches) throw new Error(`Restored profile did not verify: ${item.name}`);
10131
+ result.profiles.push({ name: item.name, action: "verified", id: current?.profile_id });
10132
+ if (comparison2.serverDefaults.length)
10133
+ result.serverDefaults.push({ profile: item.name, fields: comparison2.serverDefaults });
10134
+ continue;
10135
+ }
9944
10136
  const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
9945
10137
  result.profiles.push({
9946
10138
  name: item.name,
@@ -9951,8 +10143,11 @@ async function restoreRuntimeProfiles(api, plan) {
9951
10143
  (p) => p.profile_name === item.name
9952
10144
  );
9953
10145
  if (verified) assertTenant([verified], plan.destinationTsgId);
9954
- if (!verified?.profile_id || response.profile_id && response.profile_id !== verified.profile_id || item.source.active !== void 0 && verified.active !== item.source.active || canonical(verified.policy) !== canonical(policy))
10146
+ const comparison = compareRuntimePolicies(policy, verified?.policy);
10147
+ if (!verified?.profile_id || response.profile_id && response.profile_id !== verified.profile_id || item.source.active !== void 0 && verified.active !== item.source.active || !comparison.matches)
9955
10148
  throw new Error(`Restored profile did not verify: ${item.name}`);
10149
+ if (comparison.serverDefaults.length)
10150
+ result.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
9956
10151
  }
9957
10152
  result.complete = true;
9958
10153
  } catch (error) {
@@ -9963,10 +10158,97 @@ async function restoreRuntimeProfiles(api, plan) {
9963
10158
  }
9964
10159
  return result;
9965
10160
  }
10161
+ function mappedPolicy(item, plan, mappedTopics) {
10162
+ const policy = structuredClone(item.fallbackPolicy ?? item.source.policy);
10163
+ for (const ref of topicReferences(policy)) {
10164
+ const topic = mappedTopics.get(topicKey(ref));
10165
+ if (!topic?.topic_id)
10166
+ throw new Error(
10167
+ "Missing planned topic mapping; an existing profile cannot be verified without its destination dependencies"
10168
+ );
10169
+ ref.topic_id = topic.topic_id;
10170
+ ref.topic_name = topic.topic_name;
10171
+ ref.revision = topic.revision;
10172
+ }
10173
+ for (const ref of dlpReferences(policy)) {
10174
+ const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
10175
+ if (!mapping) continue;
10176
+ if (ref.kind === "embedded") {
10177
+ ref.value.name = mapping.destination;
10178
+ ref.value.uuid = mapping.id;
10179
+ } else ref.value.text = mapping.destination;
10180
+ if ("id" in ref.value) ref.value.id = mapping.id;
10181
+ if ("version" in ref.value) {
10182
+ if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
10183
+ ref.value.version = String(mapping.version);
10184
+ }
10185
+ }
10186
+ return policy;
10187
+ }
9966
10188
 
9967
10189
  // src/cli/commands/profile-transfer.ts
9968
10190
  var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
9969
10191
  var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
10192
+ function renderRestore(value, columns, format) {
10193
+ if (format !== "pretty" && format !== "table" && format !== "markdown")
10194
+ return formatOutput([value], columns, format);
10195
+ const markdown = format === "markdown";
10196
+ const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
10197
+ const heading = (text2) => markdown ? `### ${text2}
10198
+ ` : text2;
10199
+ const blocks = [
10200
+ heading(
10201
+ value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
10202
+ ),
10203
+ `Source TSG: ${value.sourceTsgId}
10204
+ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10205
+ ];
10206
+ for (const [key, title] of [
10207
+ ["profiles", "Profiles"],
10208
+ ["topics", "Topics"]
10209
+ ]) {
10210
+ blocks.push(heading(`${title} (${rows(key).length})`));
10211
+ blocks.push(
10212
+ formatOutput(
10213
+ rows(key),
10214
+ [
10215
+ { key: "action", label: "Action" },
10216
+ { key: "name", label: "Name" }
10217
+ ],
10218
+ markdown ? "markdown" : "table"
10219
+ ) || "(none)"
10220
+ );
10221
+ }
10222
+ if (rows("dlpMappings").length) {
10223
+ blocks.push(heading("DLP mappings"));
10224
+ blocks.push(
10225
+ formatOutput(
10226
+ rows("dlpMappings"),
10227
+ [
10228
+ { key: "source", label: "Source" },
10229
+ { key: "destination", label: "Destination" }
10230
+ ],
10231
+ markdown ? "markdown" : "table"
10232
+ )
10233
+ );
10234
+ }
10235
+ if (rows("dlpFallbacks").length) {
10236
+ blocks.push(heading("Basic DLP fallbacks \u2014 custom protection lost"));
10237
+ for (const fallback of rows("dlpFallbacks"))
10238
+ blocks.push(
10239
+ `${fallback.profile}: ${Array.isArray(fallback.replaced) ? fallback.replaced.join(", ") : ""}`
10240
+ );
10241
+ }
10242
+ if (rows("serverDefaults").length) {
10243
+ blocks.push(heading("Verified server-added defaults (source omitted these fields)"));
10244
+ for (const item of rows("serverDefaults"))
10245
+ blocks.push(
10246
+ `${item.profile}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
10247
+ );
10248
+ }
10249
+ if (value.error) blocks.push(`Error: ${value.error}`);
10250
+ return blocks.join("\n\n");
10251
+ }
9970
10252
  function maxPages(value) {
9971
10253
  if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
9972
10254
  usageError("--max-pages must be an integer from 1 to 1000");
@@ -10079,7 +10361,15 @@ function registerProfileTransferCommands(profiles2) {
10079
10361
  });
10080
10362
  const restore = profiles2.command("restore <file>").description(
10081
10363
  "Restore profile policies into the selected tenant; validate all dependencies before changes"
10082
- ).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option("--on-conflict <policy>", "Existing profile names: error, skip or update", "error").option(
10364
+ ).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option(
10365
+ "--on-conflict <policy>",
10366
+ "Existing profile names: error, verify (resume without writes), skip or update",
10367
+ "error"
10368
+ ).option(
10369
+ "--on-missing-dlp <policy>",
10370
+ "Unresolved custom DLP: error or basic (explicitly accepts loss of custom detection)",
10371
+ "error"
10372
+ ).option(
10083
10373
  "--dlp-map <source=destination>",
10084
10374
  "Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
10085
10375
  (value, prior) => [...prior, value],
@@ -10096,12 +10386,15 @@ function registerProfileTransferCommands(profiles2) {
10096
10386
  "after",
10097
10387
  examples(
10098
10388
  "airs runtime profiles restore ./profiles.json --dry-run --output json",
10389
+ "airs runtime profiles restore ./profiles.json --on-missing-dlp basic --dry-run --output json",
10099
10390
  "airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
10100
10391
  )
10101
10392
  ).action(async (file, opts) => {
10102
10393
  try {
10103
- if (!["error", "skip", "update"].includes(opts.onConflict))
10104
- throw new CliUsageError("--on-conflict must be error, skip or update");
10394
+ if (!["error", "skip", "update", "verify"].includes(opts.onConflict))
10395
+ throw new CliUsageError("--on-conflict must be error, verify, skip or update");
10396
+ if (!["error", "basic"].includes(opts.onMissingDlp))
10397
+ throw new CliUsageError("--on-missing-dlp must be error or basic");
10105
10398
  if (opts.force && !opts.expectTsg)
10106
10399
  throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
10107
10400
  const mappings = dlpMap(opts.dlpMap);
@@ -10131,7 +10424,8 @@ function registerProfileTransferCommands(profiles2) {
10131
10424
  namePrefix: opts.namePrefix,
10132
10425
  onConflict: opts.onConflict,
10133
10426
  maxPages: opts.maxPages,
10134
- dlpMap: mappings
10427
+ dlpMap: mappings,
10428
+ onMissingDlp: opts.onMissingDlp
10135
10429
  });
10136
10430
  const summary = {
10137
10431
  sourceTsgId: plan.sourceTsgId,
@@ -10145,7 +10439,9 @@ function registerProfileTransferCommands(profiles2) {
10145
10439
  dlpMappings: plan.dlpMappings.map((m) => ({
10146
10440
  source: m.source,
10147
10441
  destination: m.destination
10148
- }))
10442
+ })),
10443
+ dlpFallbacks: plan.dlpFallbacks,
10444
+ serverDefaults: plan.serverDefaults
10149
10445
  };
10150
10446
  const columns = [
10151
10447
  { key: "sourceTsgId", label: "Source TSG" },
@@ -10153,22 +10449,30 @@ function registerProfileTransferCommands(profiles2) {
10153
10449
  { key: "profiles", label: "Profiles" },
10154
10450
  { key: "topics", label: "Topics" },
10155
10451
  { key: "dlpMappings", label: "DLP mappings" },
10452
+ { key: "dlpFallbacks", label: "Basic DLP fallbacks (custom protection lost)" },
10453
+ { key: "serverDefaults", label: "Verified server-added defaults" },
10156
10454
  { key: "dryRun", label: "Dry run" },
10157
10455
  { key: "complete", label: "Complete" }
10158
10456
  ];
10457
+ for (const fallback of plan.dlpFallbacks)
10458
+ ui.warning(
10459
+ `DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
10460
+ );
10159
10461
  if (opts.dryRun) {
10160
- console.log(formatOutput([summary], columns, format === "pretty" ? "table" : format));
10462
+ console.log(renderRestore(summary, columns, format));
10161
10463
  return;
10162
10464
  }
10163
- const changing = plan.profiles.filter((p) => p.action !== "skip");
10465
+ const changing = plan.profiles.filter(
10466
+ (p) => p.action === "create" || p.action === "update"
10467
+ );
10164
10468
  if (changing.length)
10165
10469
  await confirmOrAbort(
10166
- `Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates)?`,
10470
+ `Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates, ${plan.dlpFallbacks.length} Basic DLP fallbacks losing custom detection)?`,
10167
10471
  Boolean(opts.force),
10168
10472
  { action: `restore profiles into TSG ${tsgId}` }
10169
10473
  );
10170
10474
  const result = await restoreRuntimeProfiles(api, plan);
10171
- console.log(formatOutput([{ ...result }], columns, format === "pretty" ? "table" : format));
10475
+ console.log(renderRestore({ ...result }, columns, format));
10172
10476
  if (!result.complete) {
10173
10477
  ui.error(result.error ?? "Restore incomplete");
10174
10478
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cdot65/prisma-airs-cli",
3
3
  "packageManager": "pnpm@10.6.5",
4
- "version": "5.6.0",
4
+ "version": "5.7.1",
5
5
  "description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "license": "MIT",
61
61
  "dependencies": {
62
- "@cdot65/prisma-airs-sdk": "0.29.0",
62
+ "@cdot65/prisma-airs-sdk": "0.30.0",
63
63
  "@inquirer/prompts": "^8.3.0",
64
64
  "chalk": "^5.6.2",
65
65
  "commander": "^14.0.3",