@cdot65/prisma-airs-cli 5.7.0 → 5.8.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 (2) hide show
  1. package/dist/cli/index.js +1377 -364
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -59,7 +59,7 @@ function installProcessGuards() {
59
59
  }
60
60
 
61
61
  // src/cli/program.ts
62
- import { randomUUID as randomUUID11 } from "crypto";
62
+ import { randomUUID as randomUUID12 } from "crypto";
63
63
  import { readFileSync as readFileSync4 } from "fs";
64
64
  import { dirname as dirname5, join as join5 } from "path";
65
65
  import { fileURLToPath } from "url";
@@ -142,8 +142,8 @@ function modelSecurityClientOptions(config) {
142
142
 
143
143
  // src/reports/agentguard.ts
144
144
  async function collectAgentGuardReport(client, options = {}) {
145
- const maxPages2 = options.maxPages ?? 40;
146
- if (!Number.isSafeInteger(maxPages2) || maxPages2 < 1 || maxPages2 > 100)
145
+ const maxPages3 = options.maxPages ?? 40;
146
+ if (!Number.isSafeInteger(maxPages3) || maxPages3 < 1 || maxPages3 > 100)
147
147
  throw new Error("maxPages must be between 1 and 100");
148
148
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
149
149
  const end = now();
@@ -205,7 +205,7 @@ async function collectAgentGuardReport(client, options = {}) {
205
205
  const seen = /* @__PURE__ */ new Set();
206
206
  let total;
207
207
  try {
208
- for (let page = 0; page < maxPages2; page++) {
208
+ for (let page = 0; page < maxPages3; page++) {
209
209
  const r = await read(rows.length);
210
210
  s.pages++;
211
211
  if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
@@ -222,7 +222,7 @@ async function collectAgentGuardReport(client, options = {}) {
222
222
  if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
223
223
  if (!r.rows.length) throw new Error();
224
224
  }
225
- s.notes.push(`Reached the ${maxPages2}-page budget.`);
225
+ s.notes.push(`Reached the ${maxPages3}-page budget.`);
226
226
  throw new Error();
227
227
  } catch (error) {
228
228
  if (!rows.length) throw error;
@@ -3494,7 +3494,7 @@ function registerAiGatewayInference(parent) {
3494
3494
  const outputError = (error) => controller.abort(error);
3495
3495
  let stream;
3496
3496
  let failed = false;
3497
- let failure2;
3497
+ let failure3;
3498
3498
  try {
3499
3499
  const format = await resolveOutput(command, opts, { allowed: ["pretty", "json", "yaml"] });
3500
3500
  const config = await loadConfig({ aiGwInferenceEndpoint: opts.endpoint });
@@ -3521,8 +3521,8 @@ function registerAiGatewayInference(parent) {
3521
3521
  process.once("SIGINT", interrupt);
3522
3522
  process.once("SIGTERM", interrupt);
3523
3523
  process.stdout.on("error", outputError);
3524
- const write = (text2) => new Promise((resolve8, reject) => {
3525
- process.stdout.write(text2, (error) => error ? reject(error) : resolve8());
3524
+ const write = (text2) => new Promise((resolve9, reject) => {
3525
+ process.stdout.write(text2, (error) => error ? reject(error) : resolve9());
3526
3526
  });
3527
3527
  const requestOptions = { signal: controller.signal };
3528
3528
  const result = kind === "chat" ? await client.createChatCompletion(
@@ -3557,7 +3557,7 @@ function registerAiGatewayInference(parent) {
3557
3557
  }
3558
3558
  } catch (error) {
3559
3559
  failed = true;
3560
- failure2 = error;
3560
+ failure3 = error;
3561
3561
  } finally {
3562
3562
  await stream?.cancel();
3563
3563
  controller.abort();
@@ -3565,10 +3565,10 @@ function registerAiGatewayInference(parent) {
3565
3565
  process.removeListener("SIGTERM", interrupt);
3566
3566
  process.stdout.removeListener("error", outputError);
3567
3567
  }
3568
- if (failed && failure2?.code !== "EPIPE") {
3569
- if (failure2?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
3570
- usageError(failure2 instanceof Error ? failure2.message : "Invalid inference options");
3571
- fail(failure2);
3568
+ if (failed && failure3?.code !== "EPIPE") {
3569
+ if (failure3?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
3570
+ usageError(failure3 instanceof Error ? failure3.message : "Invalid inference options");
3571
+ fail(failure3);
3572
3572
  }
3573
3573
  });
3574
3574
  }
@@ -3896,20 +3896,20 @@ function renderGatewayDetail(value, format) {
3896
3896
  );
3897
3897
  if (rendered) console.log(rendered);
3898
3898
  }
3899
- async function runList(command, opts, label, load3) {
3899
+ async function runList(command, opts, label, load4) {
3900
3900
  try {
3901
3901
  const format = await resolveOutput(command, opts);
3902
3902
  const client = await createAiGatewayClient();
3903
- renderGatewayList(responseItems(await load3(client)), format, label);
3903
+ renderGatewayList(responseItems(await load4(client)), format, label);
3904
3904
  } catch (error) {
3905
3905
  failAiGateway(error);
3906
3906
  }
3907
3907
  }
3908
- async function runDetail(command, opts, load3) {
3908
+ async function runDetail(command, opts, load4) {
3909
3909
  try {
3910
3910
  const format = await resolveOutput(command, opts);
3911
3911
  const client = await createAiGatewayClient();
3912
- renderGatewayDetail(await load3(client), format);
3912
+ renderGatewayDetail(await load4(client), format);
3913
3913
  } catch (error) {
3914
3914
  failAiGateway(error);
3915
3915
  }
@@ -5244,8 +5244,8 @@ function registerAiGatewayReportCommand(aigateway) {
5244
5244
  const iso = z2.string().datetime({ offset: true });
5245
5245
  if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
5246
5246
  usageError("Provide a valid increasing ISO-8601 window with timezone");
5247
- const maxPages2 = Number(opts.maxPages);
5248
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
5247
+ const maxPages3 = Number(opts.maxPages);
5248
+ if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
5249
5249
  usageError("--max-pages must be an integer from 1 to 100");
5250
5250
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
5251
5251
  usageError("--title must contain 1\u2013240 characters");
@@ -5269,7 +5269,7 @@ function registerAiGatewayReportCommand(aigateway) {
5269
5269
  ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
5270
5270
  const report = await collectGatewayEnvironmentReport(client, {
5271
5271
  title: opts.title,
5272
- maxPages: maxPages2,
5272
+ maxPages: maxPages3,
5273
5273
  workspace: opts.workspace,
5274
5274
  tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
5275
5275
  start,
@@ -5900,8 +5900,8 @@ async function withTimeout(promise, ms) {
5900
5900
  try {
5901
5901
  return await Promise.race([
5902
5902
  promise,
5903
- new Promise((resolve8) => {
5904
- timer = setTimeout(() => resolve8(TIMED_OUT), ms);
5903
+ new Promise((resolve9) => {
5904
+ timer = setTimeout(() => resolve9(TIMED_OUT), ms);
5905
5905
  })
5906
5906
  ]);
5907
5907
  } finally {
@@ -6186,12 +6186,12 @@ function hasBin(bin) {
6186
6186
  }
6187
6187
  }
6188
6188
  function run2(bin, args, label) {
6189
- return new Promise((resolve8, reject) => {
6189
+ return new Promise((resolve9, reject) => {
6190
6190
  const child = spawn(bin, args, { stdio: "inherit" });
6191
6191
  child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
6192
6192
  child.on("close", (code) => {
6193
6193
  if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
6194
- else resolve8();
6194
+ else resolve9();
6195
6195
  });
6196
6196
  });
6197
6197
  }
@@ -6801,8 +6801,8 @@ function registerRedTeamReportCommand(redteam, scanReport) {
6801
6801
  const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
6802
6802
  if (format !== "html" && format !== "markdown")
6803
6803
  usageError("Report output must be html or markdown");
6804
- const maxPages2 = Number(opts.maxPages);
6805
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
6804
+ const maxPages3 = Number(opts.maxPages);
6805
+ if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
6806
6806
  usageError("--max-pages must be an integer from 1 to 100");
6807
6807
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
6808
6808
  usageError("--title must contain 1\u2013240 characters");
@@ -6826,7 +6826,7 @@ function registerRedTeamReportCommand(redteam, scanReport) {
6826
6826
  ui.status("Collecting Red Team statistics and current configuration (read-only)...");
6827
6827
  const report = await collectRedTeamEnvironmentReport(client, {
6828
6828
  title: opts.title,
6829
- maxPages: maxPages2
6829
+ maxPages: maxPages3
6830
6830
  });
6831
6831
  const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
6832
6832
  if (outputPath) {
@@ -8085,9 +8085,9 @@ function registerRedteamCommand(program) {
8085
8085
  }
8086
8086
 
8087
8087
  // src/cli/commands/runtime.ts
8088
- import { randomUUID as randomUUID9 } from "crypto";
8088
+ import { randomUUID as randomUUID10 } from "crypto";
8089
8089
  import * as fs5 from "fs";
8090
- import { readFile as readFile11 } from "fs/promises";
8090
+ import { readFile as readFile12 } from "fs/promises";
8091
8091
  import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
8092
8092
  import chalk11 from "chalk";
8093
8093
 
@@ -8804,6 +8804,11 @@ var SdkDictionariesService = class {
8804
8804
  }
8805
8805
  };
8806
8806
 
8807
+ // src/cli/commands/dlp/config.ts
8808
+ async function loadDlpClientOptions() {
8809
+ return managementClientOptions(await loadConfig());
8810
+ }
8811
+
8807
8812
  // src/cli/commands/dlp/patch.ts
8808
8813
  import { readFile as readFile7 } from "fs/promises";
8809
8814
  function buildMergePatch(opts) {
@@ -8894,7 +8899,7 @@ function register(dlp) {
8894
8899
  try {
8895
8900
  const { page, size } = resolvePageParams(listCmd, opts);
8896
8901
  const includeKeywords = opts.keywords || opts.includeKeywords;
8897
- const svc = new SdkDictionariesService();
8902
+ const svc = new SdkDictionariesService(await loadDlpClientOptions());
8898
8903
  const params = {
8899
8904
  size,
8900
8905
  sort: opts.sort,
@@ -8909,17 +8914,18 @@ function register(dlp) {
8909
8914
  fail(err);
8910
8915
  }
8911
8916
  });
8912
- 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) => {
8913
8918
  try {
8919
+ const format = await resolveOutput(command, opts);
8914
8920
  const metadata = await buildMetadata(opts);
8915
8921
  if (!opts.file) throw new Error("--file is required (multipart upload)");
8916
8922
  const file = await readFile8(opts.file);
8917
- const r = await new SdkDictionariesService().create({
8923
+ const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
8918
8924
  metadata,
8919
8925
  file,
8920
8926
  includeKeywords: opts.includeKeywords
8921
8927
  });
8922
- dlpDictionaries.renderCreated(r, opts.output);
8928
+ dlpDictionaries.renderCreated(r, format);
8923
8929
  } catch (err) {
8924
8930
  usageError(err instanceof Error ? err.message : String(err));
8925
8931
  }
@@ -8928,7 +8934,9 @@ function register(dlp) {
8928
8934
  try {
8929
8935
  const includeKeywords = opts.keywords || opts.includeKeywords;
8930
8936
  dlpDictionaries.renderGet(
8931
- await new SdkDictionariesService().get(id, { includeKeywords }),
8937
+ await new SdkDictionariesService(await loadDlpClientOptions()).get(id, {
8938
+ includeKeywords
8939
+ }),
8932
8940
  await resolveOutput(getCmd, opts)
8933
8941
  );
8934
8942
  } catch (err) {
@@ -8937,12 +8945,13 @@ function register(dlp) {
8937
8945
  });
8938
8946
  group.command("replace <id>").description(
8939
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).'
8940
- ).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) => {
8941
8949
  try {
8942
8950
  const metadata = await buildMetadata(opts);
8951
+ const format = await resolveOutput(command, opts);
8943
8952
  if (!opts.file) throw new Error("--file is required (multipart upload)");
8944
8953
  const file = await readFile8(opts.file);
8945
- const r = await new SdkDictionariesService().replace(id, {
8954
+ const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
8946
8955
  metadata,
8947
8956
  file,
8948
8957
  includeKeywords: opts.includeKeywords
@@ -8950,22 +8959,23 @@ function register(dlp) {
8950
8959
  if ("kind" in r && r.kind === "fallback") {
8951
8960
  dlpDictionaries.renderReplaced204Fallback(id);
8952
8961
  } else {
8953
- dlpDictionaries.renderReplaced(r, opts.output);
8962
+ dlpDictionaries.renderReplaced(r, format);
8954
8963
  }
8955
8964
  } catch (err) {
8956
8965
  usageError(err instanceof Error ? err.message : String(err));
8957
8966
  }
8958
8967
  });
8959
- 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) => {
8960
8969
  try {
8970
+ const format = await resolveOutput(command, opts);
8961
8971
  if (opts.bodyFile && (opts.set || opts.clear)) {
8962
8972
  throw new Error("--body-file is mutually exclusive with --set/--clear");
8963
8973
  }
8964
8974
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
8965
8975
  dlpDictionaries.renderPatched(
8966
8976
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
8967
- await new SdkDictionariesService().patch(id, body),
8968
- opts.output
8977
+ await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
8978
+ format
8969
8979
  );
8970
8980
  } catch (err) {
8971
8981
  usageError(err instanceof Error ? err.message : String(err));
@@ -8973,7 +8983,7 @@ function register(dlp) {
8973
8983
  });
8974
8984
  group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
8975
8985
  try {
8976
- await new SdkDictionariesService().delete(id);
8986
+ await new SdkDictionariesService(await loadDlpClientOptions()).delete(id);
8977
8987
  dlpDictionaries.renderDeleted(id);
8978
8988
  } catch (err) {
8979
8989
  fail(err);
@@ -9146,7 +9156,7 @@ function register2(dlp) {
9146
9156
  listCmd.action(async (opts) => {
9147
9157
  try {
9148
9158
  const { page, size } = resolvePageParams(listCmd, opts);
9149
- const svc = new SdkDataFilteringProfilesService();
9159
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9150
9160
  const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9151
9161
  const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort });
9152
9162
  dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
@@ -9156,20 +9166,21 @@ function register2(dlp) {
9156
9166
  });
9157
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) => {
9158
9168
  try {
9159
- const svc = new SdkDataFilteringProfilesService();
9169
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9160
9170
  dlpFilteringProfiles.renderGet(await svc.get(id), await resolveOutput(getCmd, opts));
9161
9171
  } catch (err) {
9162
9172
  fail(err);
9163
9173
  }
9164
9174
  });
9165
- 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) => {
9166
9176
  try {
9167
9177
  const body = await resolveReplaceBody(opts);
9168
- const svc = new SdkDataFilteringProfilesService();
9178
+ const format = await resolveOutput(command, opts);
9179
+ const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9169
9180
  dlpFilteringProfiles.renderReplaced(
9170
9181
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9171
9182
  await svc.replace(id, body),
9172
- opts.output
9183
+ format
9173
9184
  );
9174
9185
  } catch (err) {
9175
9186
  usageError(err instanceof Error ? err.message : String(err));
@@ -9314,7 +9325,7 @@ function register4(dlp) {
9314
9325
  listCmd.action(async (opts) => {
9315
9326
  try {
9316
9327
  const { page, size } = resolvePageParams(listCmd, opts);
9317
- const svc = new SdkDataPatternsService();
9328
+ const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9318
9329
  const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9319
9330
  dlpPatterns.renderList(
9320
9331
  result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
@@ -9324,22 +9335,25 @@ function register4(dlp) {
9324
9335
  fail(err);
9325
9336
  }
9326
9337
  });
9327
- writeFlags(group.command("create").description("Create a data pattern")).action(async (opts) => {
9328
- try {
9329
- const body = await resolveWriteBody(opts);
9330
- dlpPatterns.renderCreated(
9331
- // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9332
- await new SdkDataPatternsService().create(body),
9333
- opts.output
9334
- );
9335
- } catch (err) {
9336
- 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
+ }
9337
9351
  }
9338
- });
9352
+ );
9339
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) => {
9340
9354
  try {
9341
9355
  dlpPatterns.renderGet(
9342
- await new SdkDataPatternsService().get(id),
9356
+ await new SdkDataPatternsService(await loadDlpClientOptions()).get(id),
9343
9357
  await resolveOutput(getCmd, opts)
9344
9358
  );
9345
9359
  } catch (err) {
@@ -9347,13 +9361,14 @@ function register4(dlp) {
9347
9361
  }
9348
9362
  });
9349
9363
  writeFlags(group.command("replace <id>").description("Full-replace a data pattern (PUT)")).action(
9350
- async (id, opts) => {
9364
+ async (id, opts, command) => {
9351
9365
  try {
9366
+ const format = await resolveOutput(command, opts);
9352
9367
  const body = await resolveWriteBody(opts);
9353
9368
  dlpPatterns.renderReplaced(
9354
9369
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9355
- await new SdkDataPatternsService().replace(id, body),
9356
- opts.output
9370
+ await new SdkDataPatternsService(await loadDlpClientOptions()).replace(id, body),
9371
+ format
9357
9372
  );
9358
9373
  } catch (err) {
9359
9374
  usageError(err instanceof Error ? err.message : String(err));
@@ -9362,16 +9377,17 @@ function register4(dlp) {
9362
9377
  );
9363
9378
  group.command("patch <id>").description(
9364
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"'.`
9365
- ).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) => {
9366
9381
  try {
9382
+ const format = await resolveOutput(command, opts);
9367
9383
  if (opts.bodyFile && (opts.set || opts.clear)) {
9368
9384
  throw new Error("--body-file is mutually exclusive with --set/--clear");
9369
9385
  }
9370
9386
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9371
9387
  dlpPatterns.renderPatched(
9372
9388
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9373
- await new SdkDataPatternsService().patch(id, body),
9374
- opts.output
9389
+ await new SdkDataPatternsService(await loadDlpClientOptions()).patch(id, body),
9390
+ format
9375
9391
  );
9376
9392
  } catch (err) {
9377
9393
  usageError(err instanceof Error ? err.message : String(err));
@@ -9379,7 +9395,7 @@ function register4(dlp) {
9379
9395
  });
9380
9396
  group.command("delete <id>").description("Soft-delete (archive) a data pattern").action(async (id) => {
9381
9397
  try {
9382
- await new SdkDataPatternsService().delete(id);
9398
+ await new SdkDataPatternsService(await loadDlpClientOptions()).delete(id);
9383
9399
  dlpPatterns.renderArchived(id);
9384
9400
  } catch (err) {
9385
9401
  fail(err);
@@ -9455,7 +9471,7 @@ function register5(dlp) {
9455
9471
  listCmd.action(async (opts) => {
9456
9472
  try {
9457
9473
  const { page, size } = resolvePageParams(listCmd, opts);
9458
- const svc = new SdkDataProfilesService();
9474
+ const svc = new SdkDataProfilesService(await loadDlpClientOptions());
9459
9475
  const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9460
9476
  dlpProfiles.renderList(
9461
9477
  result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
@@ -9465,22 +9481,25 @@ function register5(dlp) {
9465
9481
  fail(err);
9466
9482
  }
9467
9483
  });
9468
- writeFlags2(group.command("create").description("Create a data profile")).action(async (opts) => {
9469
- try {
9470
- const body = await resolveWriteBody2(opts);
9471
- dlpProfiles.renderCreated(
9472
- // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9473
- await new SdkDataProfilesService().create(body),
9474
- opts.output
9475
- );
9476
- } catch (err) {
9477
- 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
+ }
9478
9497
  }
9479
- });
9498
+ );
9480
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) => {
9481
9500
  try {
9482
9501
  dlpProfiles.renderGet(
9483
- await new SdkDataProfilesService().get(id),
9502
+ await new SdkDataProfilesService(await loadDlpClientOptions()).get(id),
9484
9503
  await resolveOutput(getCmd, opts)
9485
9504
  );
9486
9505
  } catch (err) {
@@ -9488,13 +9507,14 @@ function register5(dlp) {
9488
9507
  }
9489
9508
  });
9490
9509
  writeFlags2(group.command("replace <id>").description("Full-replace a data profile (PUT)")).action(
9491
- async (id, opts) => {
9510
+ async (id, opts, command) => {
9492
9511
  try {
9512
+ const format = await resolveOutput(command, opts);
9493
9513
  const body = await resolveWriteBody2(opts);
9494
9514
  dlpProfiles.renderReplaced(
9495
9515
  // biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
9496
- await new SdkDataProfilesService().replace(id, body),
9497
- opts.output
9516
+ await new SdkDataProfilesService(await loadDlpClientOptions()).replace(id, body),
9517
+ format
9498
9518
  );
9499
9519
  } catch (err) {
9500
9520
  usageError(err instanceof Error ? err.message : String(err));
@@ -9503,16 +9523,17 @@ function register5(dlp) {
9503
9523
  );
9504
9524
  group.command("patch <id>").description(
9505
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"'.`
9506
- ).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) => {
9507
9527
  try {
9528
+ const format = await resolveOutput(command, opts);
9508
9529
  if (opts.bodyFile && (opts.set || opts.clear)) {
9509
9530
  throw new Error("--body-file is mutually exclusive with --set/--clear");
9510
9531
  }
9511
9532
  const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9512
9533
  dlpProfiles.renderPatched(
9513
9534
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9514
- await new SdkDataProfilesService().patch(id, body),
9515
- opts.output
9535
+ await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
9536
+ format
9516
9537
  );
9517
9538
  } catch (err) {
9518
9539
  usageError(err instanceof Error ? err.message : String(err));
@@ -9529,17 +9550,7 @@ function register5(dlp) {
9529
9550
  });
9530
9551
  }
9531
9552
 
9532
- // src/cli/commands/dlp/index.ts
9533
- function registerDlpCommands(runtime) {
9534
- const dlp = runtime.command("dlp").description("DLP management (filtering-profiles, patterns, profiles, dictionaries, generate)");
9535
- register2(dlp);
9536
- register4(dlp);
9537
- register5(dlp);
9538
- register(dlp);
9539
- register3(dlp);
9540
- }
9541
-
9542
- // src/cli/commands/profile-transfer.ts
9553
+ // src/cli/commands/dlp/transfer.ts
9543
9554
  import { randomUUID as randomUUID7 } from "crypto";
9544
9555
  import { readFile as readFile9, stat } from "fs/promises";
9545
9556
  import { extname, resolve as resolve5 } from "path";
@@ -9547,222 +9558,1224 @@ import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot
9547
9558
  import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
9548
9559
  import { ZodError } from "zod";
9549
9560
 
9550
- // src/backup/runtime-profiles.ts
9561
+ // src/backup/dlp-resources.ts
9551
9562
  import {
9552
- CreateSecurityProfileRequestSchema,
9553
- CustomTopicSchema,
9554
- PolicySchema,
9555
- SecurityProfileSchema
9563
+ AdvancedDataProfileRequestSchema,
9564
+ DataPatternRequestSchema,
9565
+ DataPatternResponseSchema,
9566
+ DataProfileResponseSchema,
9567
+ DictionaryRequestSchema,
9568
+ DictionaryResponseSchema
9556
9569
  } from "@cdot65/prisma-airs-sdk";
9557
9570
  import { z as z5 } from "zod";
9558
-
9559
- // src/backup/runtime-policy.ts
9560
- function object(value) {
9561
- return value !== null && typeof value === "object" && !Array.isArray(value);
9562
- }
9563
- function compareRuntimePolicies(expected, actual) {
9564
- const serverDefaults = [];
9565
- const differences = [];
9566
- const visit = (source, target, path3, insideToxicCategories = false, insideTopicGuardrails = false) => {
9567
- if (source === target) return;
9568
- if (Array.isArray(source) && Array.isArray(target)) {
9569
- if (source.length !== target.length) differences.push(`${path3}.length`);
9570
- source.forEach((item, index) => {
9571
- visit(
9572
- item,
9573
- target[index],
9574
- `${path3}[${index}]`,
9575
- insideToxicCategories,
9576
- insideTopicGuardrails
9577
- );
9578
- });
9579
- return;
9580
- }
9581
- if (object(source) && object(target)) {
9582
- for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
9583
- const field = `${path3}.${key}`;
9584
- if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
9585
- const model = /^policy\.ai-security-profiles\[\d+\]\.model-configuration/;
9586
- const relative = path3.replace(model, "model");
9587
- let allowed;
9588
- if (relative === "model.data-protection" && key === "database-security") allowed = null;
9589
- if (key === "severity") {
9590
- if (/^model\.data-protection\.database-security\[\d+\]$/.test(relative)) {
9591
- const values = {
9592
- "database-security-create": "medium",
9593
- "database-security-read": "low",
9594
- "database-security-update": "medium",
9595
- "database-security-delete": "high"
9596
- };
9597
- if (source.name === target.name && typeof source.name === "string")
9598
- allowed = Object.hasOwn(values, source.name) ? values[source.name] : void 0;
9599
- }
9600
- if (relative === "model.data-protection.source-code-detection") allowed = "high";
9601
- if (relative === "model.app-protection.malicious-code-protection" && source.name === "malicious-code" && target.name === source.name)
9602
- allowed = "high";
9603
- 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)
9604
- allowed = "medium";
9605
- 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)
9606
- allowed = "medium";
9607
- if (/^model\.agent-protection\[\d+\]$/.test(relative) && source.name === "agent-security" && target.name === source.name)
9608
- allowed = "medium";
9609
- }
9610
- if (relative === "model.app-protection" && key === "url-detected-severity")
9611
- allowed = "low";
9612
- if (relative === "model.app-protection" && key === "default-url-category" && object(target[key])) {
9613
- const value = target[key];
9614
- if (Object.keys(value).length === 1 && value.member === null) {
9615
- serverDefaults.push(field);
9616
- continue;
9617
- }
9618
- }
9619
- 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])) {
9620
- const value = target[key];
9621
- if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
9622
- serverDefaults.push(field);
9623
- continue;
9624
- }
9625
- }
9626
- if (allowed !== void 0 && target[key] === allowed) {
9627
- serverDefaults.push(field);
9628
- continue;
9629
- }
9630
- differences.push(field);
9631
- } else if (!Object.hasOwn(target, key)) differences.push(field);
9632
- else
9633
- visit(
9634
- source[key],
9635
- target[key],
9636
- field,
9637
- key === "toxic-category-list" && source.name === "toxic-content" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
9638
- path3
9639
- ),
9640
- key === "topic-list" && source.name === "topic-guardrails" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
9641
- path3
9642
- ) || insideTopicGuardrails && key === "topic" && source.action === "block" && target.action === "block"
9643
- );
9644
- }
9645
- return;
9646
- }
9647
- differences.push(path3);
9648
- };
9649
- visit(expected, actual, "policy");
9650
- return { matches: differences.length === 0, serverDefaults, differences };
9651
- }
9652
-
9653
- // src/backup/runtime-profiles.ts
9654
- var RuntimeProfilesBackupSchema = z5.object({
9571
+ var DLP_RESOURCE_KINDS = ["dictionaries", "patterns", "profiles"];
9572
+ var DlpResourcesBackupSchema = z5.object({
9655
9573
  version: z5.literal(1),
9656
- resourceType: z5.literal("runtime-security-profiles"),
9574
+ resourceType: z5.literal("dlp-resources"),
9657
9575
  exportedAt: z5.string().datetime({ offset: true }),
9658
9576
  source: z5.object({ tsgId: z5.string().min(1) }).strict(),
9659
- profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
9660
- topics: z5.array(CustomTopicSchema).max(1e4)
9577
+ dictionaries: z5.array(DictionaryResponseSchema).max(1e4),
9578
+ patterns: z5.array(DataPatternResponseSchema).max(1e4),
9579
+ profiles: z5.array(DataProfileResponseSchema).max(1e4)
9661
9580
  }).strict();
9662
- function object2(value) {
9581
+ function object(value) {
9663
9582
  return value !== null && typeof value === "object" && !Array.isArray(value);
9664
9583
  }
9584
+ var PORTABLE_TECHNIQUES = /* @__PURE__ */ new Set(["regex", "weighted_regex", "file_property"]);
9585
+ var RETIRED_PATTERN = /* @__PURE__ */ new Set(["deleted", "disabled"]);
9586
+ var RETIRED_PROFILE = /* @__PURE__ */ new Set(["deleted", "disabled"]);
9587
+ var SERVER_METADATA_KEYS = /* @__PURE__ */ new Set([
9588
+ "id",
9589
+ "version",
9590
+ "status",
9591
+ "profile_status",
9592
+ "tenant_id",
9593
+ "license_type",
9594
+ "is_parent_managed",
9595
+ "audit_metadata",
9596
+ "type",
9597
+ "advance_data_patterns_rule_request",
9598
+ "dictionary_metadata",
9599
+ "detection_technique",
9600
+ "detection_sub_technique",
9601
+ "attributes",
9602
+ "keywords",
9603
+ "tags"
9604
+ ]);
9665
9605
  function pagesLimit(value) {
9666
9606
  if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
9667
9607
  throw new Error("maxPages must be an integer from 1 to 1000");
9668
9608
  }
9669
- async function offsetPages(fetch, maxPages2) {
9670
- pagesLimit(maxPages2);
9671
- const items = [];
9672
- let offset = 0;
9673
- for (let page = 0; page < maxPages2; page++) {
9674
- const result = await fetch(offset);
9675
- items.push(...result.items);
9676
- if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
9677
- if (result.next === void 0 || result.next === 0) return items;
9678
- if (!result.items.length || !Number.isSafeInteger(result.next) || result.next <= offset)
9679
- throw new Error("Inventory pagination did not advance");
9680
- offset = result.next;
9681
- }
9682
- throw new Error("Inventory is incomplete: increase --max-pages before exporting or restoring");
9683
- }
9684
- function latestProfiles(profiles2) {
9685
- const names = /* @__PURE__ */ new Map();
9686
- const revisions = /* @__PURE__ */ new Set();
9687
- for (const profile of profiles2) {
9688
- assertName(profile.profile_name);
9689
- const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
9690
- if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
9691
- revisions.add(identity2);
9692
- const prior = names.get(profile.profile_name);
9693
- if (prior && prior.profile_id !== profile.profile_id)
9694
- throw new Error(`Ambiguous profile name: ${profile.profile_name}`);
9695
- if (!prior || (profile.revision ?? 0) > (prior.revision ?? 0))
9696
- names.set(profile.profile_name, profile);
9697
- }
9698
- return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
9699
- }
9700
9609
  function assertName(name) {
9701
9610
  if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
9702
- throw new Error("Profile/topic names must be nonempty and contain no control characters");
9611
+ throw new Error("DLP resource names must be nonempty and contain no control characters");
9703
9612
  }
9704
- function assertTenant(profiles2, tsgId) {
9705
- if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
9706
- throw new Error("Destination/source profile tenant identity does not match the selected TSG");
9613
+ async function springPages(fetch, maxPages3, label) {
9614
+ pagesLimit(maxPages3);
9615
+ const items = [];
9616
+ for (let page = 0; page < maxPages3; page++) {
9617
+ const result = await fetch(page);
9618
+ items.push(...result.content);
9619
+ if (items.length > 1e4)
9620
+ throw new Error(`Inventory of ${label} exceeds the 10000-record safety limit`);
9621
+ const last = result.last ?? (result.totalPages !== void 0 ? page + 1 >= result.totalPages : result.content.length < 100);
9622
+ if (last) return items;
9623
+ if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
9624
+ }
9625
+ throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
9707
9626
  }
9708
- async function profiles(api, maxPages2) {
9709
- return latestProfiles(
9710
- await offsetPages(async (offset) => {
9711
- const page = await api.profiles.list({ offset, limit: 100, latest: true });
9712
- return { items: page.ai_profiles, next: page.next_offset };
9713
- }, maxPages2)
9714
- );
9627
+ async function inventories(api, maxPages3) {
9628
+ return {
9629
+ dictionaries: await springPages(
9630
+ (page) => api.dictionaries.list({ page, size: 100, keywords: true }),
9631
+ maxPages3,
9632
+ "dictionaries"
9633
+ ),
9634
+ patterns: await springPages(
9635
+ (page) => api.patterns.list({ page, size: 100 }),
9636
+ maxPages3,
9637
+ "data patterns"
9638
+ ),
9639
+ profiles: await springPages(
9640
+ (page) => api.profiles.list({ page, size: 100 }),
9641
+ maxPages3,
9642
+ "data profiles"
9643
+ )
9644
+ };
9715
9645
  }
9716
- async function topics(api, maxPages2) {
9717
- return offsetPages(async (offset) => {
9718
- const page = await api.topics.list({ offset, limit: 100 });
9719
- return { items: page.custom_topics, next: page.next_offset };
9720
- }, maxPages2);
9646
+ function canonical(value) {
9647
+ const order = (v) => Array.isArray(v) ? v.map(order) : object(v) ? Object.fromEntries(
9648
+ Object.entries(v).filter(([, child]) => child !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
9649
+ ) : v;
9650
+ return JSON.stringify(order(value));
9721
9651
  }
9722
- function topicReferences(policy) {
9723
- const refs = [];
9724
- const visit = (value) => {
9652
+ function tenantIdentity(records) {
9653
+ const values = new Set(
9654
+ records.map((record) => record.tenant_id).filter((value) => value != null)
9655
+ );
9656
+ if (values.size > 1) throw new Error("Inventory reports mixed DLP tenant identities");
9657
+ return [...values][0];
9658
+ }
9659
+ function ruleLeaves(profile) {
9660
+ const leaves = [];
9661
+ const visit = (node, depth) => {
9662
+ if (depth > 100) throw new Error("Unsupported detection rule: expression tree too deep");
9663
+ if (!object(node)) return;
9664
+ if (object(node.rule_item)) {
9665
+ if (typeof node.rule_item.detection_technique !== "string")
9666
+ throw new Error("Unsupported detection rule: leaf without a detection technique");
9667
+ leaves.push({ value: node.rule_item, technique: node.rule_item.detection_technique });
9668
+ }
9669
+ if (Array.isArray(node.sub_expressions))
9670
+ for (const child of node.sub_expressions) visit(child, depth + 1);
9671
+ };
9672
+ for (const rule of profile.detection_rules ?? []) {
9673
+ if (!object(rule)) throw new Error("Unsupported detection rule shape");
9674
+ if (rule.rule_type !== "expression_tree")
9675
+ throw new Error(`Unsupported detection rule type: ${String(rule.rule_type)}`);
9676
+ visit(rule.expression_tree, 0);
9677
+ }
9678
+ return leaves;
9679
+ }
9680
+ function assertKnownReferences(body, known, label) {
9681
+ const visit = (value, path3) => {
9725
9682
  if (Array.isArray(value)) {
9726
- value.forEach(visit);
9683
+ value.forEach((v, i) => {
9684
+ visit(v, `${path3}[${i}]`);
9685
+ });
9727
9686
  return;
9728
9687
  }
9729
- if (!object2(value)) return;
9730
- if ("topic_id" in value) {
9731
- if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
9732
- throw new Error("Invalid topic identity in profile policy");
9733
- refs.push(value);
9688
+ if (!object(value)) return;
9689
+ for (const [key, child] of Object.entries(value)) {
9690
+ if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !(known.has(value) && key === "id"))
9691
+ throw new Error(`Unsupported cross-tenant reference in ${label} at ${path3}.${key}`);
9692
+ visit(child, `${path3}.${key}`);
9734
9693
  }
9735
- Object.values(value).forEach(visit);
9736
9694
  };
9737
- visit(policy);
9738
- return refs;
9695
+ visit(body, label);
9739
9696
  }
9740
- function topicKey(ref) {
9741
- return JSON.stringify([ref.topic_id, ref.revision]);
9697
+ function uniqueNames(records, label) {
9698
+ const names = /* @__PURE__ */ new Set();
9699
+ for (const record of records) {
9700
+ const name = record.name;
9701
+ if (!name) throw new Error(`Backup contains a ${label} without a name`);
9702
+ assertName(name);
9703
+ if (names.has(name)) throw new Error(`Backup contains duplicate ${label} names: ${name}`);
9704
+ names.add(name);
9705
+ }
9742
9706
  }
9743
- async function backupRuntimeProfiles(api, tsgId, options = {}) {
9707
+ async function backupDlpResources(api, tsgId, options = {}) {
9744
9708
  if (!tsgId) throw new Error("Source TSG ID is required");
9745
- const maxPages2 = options.maxPages ?? 100;
9746
- let selected = await profiles(api, maxPages2);
9747
- assertTenant(selected, tsgId);
9748
- if (options.profile)
9749
- selected = selected.filter(
9750
- (p) => p.profile_id === options.profile || p.profile_name === options.profile
9751
- );
9752
- if (!selected.length) throw new Error("No matching profiles to back up");
9753
- if (options.profile && selected.length !== 1) throw new Error("Ambiguous profile selector");
9754
- for (const profile of selected) {
9755
- if (!profile.policy) throw new Error(`Profile has no policy: ${profile.profile_name}`);
9756
- PolicySchema.parse(profile.policy);
9709
+ const maxPages3 = options.maxPages ?? 100;
9710
+ const kinds = options.resources ?? [...DLP_RESOURCE_KINDS];
9711
+ if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)))
9712
+ throw new Error("resources must name dictionaries, patterns and/or profiles");
9713
+ const inventory2 = await inventories(api, maxPages3);
9714
+ tenantIdentity([...inventory2.patterns, ...inventory2.profiles]);
9715
+ const dictionaries = /* @__PURE__ */ new Map();
9716
+ const patterns = /* @__PURE__ */ new Map();
9717
+ const addDictionary = (record) => {
9718
+ if (!record.id) throw new Error("Inventory dictionary has no id");
9719
+ if (record.type !== "predefined" && !Array.isArray(record.keywords))
9720
+ throw new Error(`Dictionary keywords were not returned by the region: ${record.name}`);
9721
+ dictionaries.set(record.id, structuredClone(record));
9722
+ };
9723
+ const addPattern = (record) => {
9724
+ if (!record.id) throw new Error("Inventory data pattern has no id");
9725
+ patterns.set(record.id, structuredClone(record));
9726
+ };
9727
+ if (kinds.includes("dictionaries")) {
9728
+ for (const record of inventory2.dictionaries)
9729
+ if (record.type !== "predefined") addDictionary(record);
9730
+ }
9731
+ if (kinds.includes("patterns")) {
9732
+ for (const record of inventory2.patterns)
9733
+ if (record.type !== "predefined" && !RETIRED_PATTERN.has(record.status ?? "active"))
9734
+ addPattern(record);
9735
+ }
9736
+ const profiles2 = [];
9737
+ const skipped = [];
9738
+ if (kinds.includes("profiles")) {
9739
+ const patternById = new Map(inventory2.patterns.map((p) => [p.id ?? "", p]));
9740
+ const dictionaryById = new Map(inventory2.dictionaries.map((d) => [d.id ?? "", d]));
9741
+ for (const profile of inventory2.profiles) {
9742
+ if (profile.type === "predefined") continue;
9743
+ if (RETIRED_PROFILE.has(profile.profile_status ?? "active")) continue;
9744
+ const name = profile.name ?? "(unnamed)";
9745
+ try {
9746
+ for (const leaf of ruleLeaves(profile)) {
9747
+ const id = leaf.value.id;
9748
+ if (id == null || id === "") {
9749
+ if (leaf.value.edm_dataset_id != null)
9750
+ throw new Error("references an EDM dataset directly");
9751
+ continue;
9752
+ }
9753
+ if (typeof id !== "string") throw new Error("has a non-string rule reference id");
9754
+ if (leaf.technique === "dictionary") {
9755
+ const record = dictionaryById.get(id);
9756
+ if (!record) throw new Error(`references an unknown dictionary: ${id}`);
9757
+ if (leaf.value.version != null)
9758
+ throw new Error(`references a version-pinned dictionary: ${id}`);
9759
+ addDictionary(record);
9760
+ } else {
9761
+ const record = patternById.get(id);
9762
+ if (!record) throw new Error(`references an unknown data pattern: ${id}`);
9763
+ if (leaf.value.version != null && record.version !== leaf.value.version)
9764
+ throw new Error(
9765
+ `references data pattern version ${leaf.value.version}, but the catalog holds version ${record.version ?? "unknown"}: ${id}`
9766
+ );
9767
+ addPattern(record);
9768
+ }
9769
+ }
9770
+ profiles2.push(structuredClone(profile));
9771
+ } catch (error) {
9772
+ const reason = error instanceof Error ? error.message : "unsupported rule";
9773
+ if (!options.skipUnsupported)
9774
+ throw new Error(`Profile cannot be exported: ${name} ${reason}`);
9775
+ skipped.push({ profile: name, reason });
9776
+ }
9777
+ }
9757
9778
  }
9758
- const refs = selected.flatMap((profile) => topicReferences(profile.policy));
9759
- const definitions = refs.length ? await topics(api, maxPages2) : [];
9760
- const needed = /* @__PURE__ */ new Map();
9761
- for (const ref of refs) {
9762
- const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
9763
- if (matches.length !== 1 || matches[0].topic_name !== ref.topic_name)
9764
- throw new Error(
9765
- `Cannot export the exact referenced topic revision: ${ref.topic_name} revision ${ref.revision}`
9779
+ const byName = (a, b) => (a.name ?? "").localeCompare(b.name ?? "");
9780
+ const dictionaryRecords = [...dictionaries.values()].sort(byName);
9781
+ const patternRecords = [...patterns.values()].sort(byName);
9782
+ profiles2.sort(byName);
9783
+ uniqueNames(dictionaryRecords, "dictionary");
9784
+ uniqueNames(patternRecords, "data pattern");
9785
+ uniqueNames(profiles2, "data profile");
9786
+ if (!dictionaryRecords.length && !patternRecords.length && !profiles2.length)
9787
+ throw new Error("No matching DLP resources to back up");
9788
+ const backup = DlpResourcesBackupSchema.parse({
9789
+ version: 1,
9790
+ resourceType: "dlp-resources",
9791
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
9792
+ source: { tsgId },
9793
+ dictionaries: dictionaryRecords,
9794
+ patterns: patternRecords,
9795
+ profiles: profiles2
9796
+ });
9797
+ return { backup, skipped };
9798
+ }
9799
+ function compareEcho(request, readBack) {
9800
+ const differences = [];
9801
+ const serverAdded = [];
9802
+ const visit = (source, target, path3, top) => {
9803
+ const a = source ?? null;
9804
+ const b = target ?? null;
9805
+ if (a === null && b === null) return;
9806
+ if (a === null) {
9807
+ serverAdded.push(path3);
9808
+ return;
9809
+ }
9810
+ if (b === null) {
9811
+ differences.push(path3);
9812
+ return;
9813
+ }
9814
+ if (Array.isArray(a) || Array.isArray(b)) {
9815
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
9816
+ differences.push(path3);
9817
+ return;
9818
+ }
9819
+ a.forEach((item, index) => {
9820
+ visit(item, b[index], `${path3}[${index}]`, false);
9821
+ });
9822
+ return;
9823
+ }
9824
+ if (object(a) && object(b)) {
9825
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
9826
+ if (top && !(key in a) && SERVER_METADATA_KEYS.has(key)) continue;
9827
+ visit(a[key], b[key], `${path3}.${key}`, false);
9828
+ }
9829
+ return;
9830
+ }
9831
+ if (a !== b) differences.push(path3);
9832
+ };
9833
+ visit(request, readBack, "body", true);
9834
+ return { matches: differences.length === 0, differences, serverAdded };
9835
+ }
9836
+ function keywordSet(value) {
9837
+ if (!Array.isArray(value) || value.some((keyword) => typeof keyword !== "string"))
9838
+ return void 0;
9839
+ return [...value].sort();
9840
+ }
9841
+ function dictionaryEcho(metadata, record, keywords) {
9842
+ const { original_file_name, ...projection } = metadata;
9843
+ const echo = compareEcho(projection, record);
9844
+ const echoedFile = record?.dictionary_metadata?.original_file_name;
9845
+ const echoedKeywords = keywordSet(record?.keywords);
9846
+ const matches = echo.matches && (echoedFile == null || echoedFile === original_file_name) && echoedKeywords !== void 0 && canonical(echoedKeywords) === canonical(keywords);
9847
+ return { matches, serverAdded: echo.serverAdded };
9848
+ }
9849
+ function dictionaryRequest(source, name) {
9850
+ const metadata = {
9851
+ category: source.category,
9852
+ name,
9853
+ original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
9854
+ region_name: source.region_name,
9855
+ ...source.description != null ? { description: source.description } : {},
9856
+ ...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {}
9857
+ };
9858
+ const parsed = DictionaryRequestSchema.safeParse(metadata);
9859
+ if (!parsed.success)
9860
+ throw new Error(`Dictionary cannot be rebuilt from the backup: ${source.name}`);
9861
+ return parsed.data;
9862
+ }
9863
+ function patternRequest(source, name) {
9864
+ const body = {
9865
+ name,
9866
+ type: source.type,
9867
+ detection_config: source.detection_config,
9868
+ ...source.matching_rules != null ? { matching_rules: source.matching_rules } : {},
9869
+ ...source.description != null ? { description: source.description } : {},
9870
+ ...source.tags != null ? { tags: source.tags } : {}
9871
+ };
9872
+ const parsed = DataPatternRequestSchema.safeParse(structuredClone(body));
9873
+ if (!parsed.success)
9874
+ throw new Error(`Data pattern cannot be rebuilt from the backup: ${source.name}`);
9875
+ return parsed.data;
9876
+ }
9877
+ function portablePattern(record) {
9878
+ return PORTABLE_TECHNIQUES.has(record.detection_config?.technique ?? "") && record.detection_config?.technique !== void 0;
9879
+ }
9880
+ function matchByName(records, name, retired, label) {
9881
+ const matches = records.filter((record) => record.name === name && !retired(record));
9882
+ if (matches.length > 1) throw new Error(`Ambiguous destination ${label} name: ${name}`);
9883
+ return matches[0];
9884
+ }
9885
+ async function planDlpResourcesRestore(api, input2, destinationTsgId, options = {}) {
9886
+ const backup = DlpResourcesBackupSchema.parse(input2);
9887
+ if (!destinationTsgId) throw new Error("Destination TSG ID is required");
9888
+ const maxPages3 = options.maxPages ?? 100;
9889
+ pagesLimit(maxPages3);
9890
+ const prefix = options.namePrefix ?? "";
9891
+ if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
9892
+ throw new Error("Invalid --name-prefix");
9893
+ const conflict = options.onConflict ?? "error";
9894
+ if (!["error", "verify", "skip"].includes(conflict)) throw new Error("Invalid conflict policy");
9895
+ const patternMap2 = options.patternMap ?? {};
9896
+ uniqueNames(backup.dictionaries, "dictionary");
9897
+ uniqueNames(backup.patterns, "data pattern");
9898
+ uniqueNames(backup.profiles, "data profile");
9899
+ const unknownMapped = Object.keys(patternMap2).filter(
9900
+ (name) => !backup.patterns.some((p) => p.name === name)
9901
+ );
9902
+ if (unknownMapped.length)
9903
+ throw new Error(`--pattern-map names absent from the backup: ${unknownMapped.join(", ")}`);
9904
+ const crossTenant = backup.source.tsgId !== destinationTsgId;
9905
+ const destination = await inventories(api, maxPages3);
9906
+ const sourceTenant = tenantIdentity([...backup.patterns, ...backup.profiles]);
9907
+ const destinationTenant = tenantIdentity([...destination.patterns, ...destination.profiles]);
9908
+ if (sourceTenant !== void 0 && destinationTenant !== void 0) {
9909
+ if (crossTenant && sourceTenant === destinationTenant)
9910
+ throw new Error(
9911
+ "Destination credentials report the source DLP tenant; check the selected tenant before restoring"
9912
+ );
9913
+ if (!crossTenant && sourceTenant !== destinationTenant)
9914
+ throw new Error(
9915
+ "Destination DLP tenant does not match the backup source; use distinct tenant credentials deliberately"
9916
+ );
9917
+ }
9918
+ const dictionaryPlans = [];
9919
+ for (const source of backup.dictionaries) {
9920
+ if (!source.id || !source.name) throw new Error("Backup dictionary lacks identity");
9921
+ if (source.type === "predefined") {
9922
+ const match2 = matchByName(destination.dictionaries, source.name, () => false, "dictionary");
9923
+ if (!match2?.id || match2.type !== "predefined")
9924
+ throw new Error(`Missing predefined destination dictionary: ${source.name}`);
9925
+ dictionaryPlans.push({
9926
+ sourceId: source.id,
9927
+ name: source.name,
9928
+ action: "resolve",
9929
+ existing: structuredClone(match2)
9930
+ });
9931
+ continue;
9932
+ }
9933
+ const name = `${prefix}${source.name}`;
9934
+ assertName(name);
9935
+ const metadata = dictionaryRequest(source, name);
9936
+ const keywords = keywordSet(source.keywords);
9937
+ if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
9938
+ const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
9939
+ if (!match) {
9940
+ dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
9941
+ continue;
9942
+ }
9943
+ if (!match.id) throw new Error(`Destination dictionary has no identity: ${name}`);
9944
+ if (!dictionaryEcho(metadata, match, keywords).matches)
9945
+ throw new Error(
9946
+ `Destination dictionary differs: ${name}; use --name-prefix to avoid changing a shared dictionary`
9947
+ );
9948
+ dictionaryPlans.push({
9949
+ sourceId: source.id,
9950
+ name,
9951
+ action: "reuse",
9952
+ metadata,
9953
+ keywords,
9954
+ existing: structuredClone(match)
9955
+ });
9956
+ }
9957
+ const patternPlans = [];
9958
+ for (const source of backup.patterns) {
9959
+ if (!source.id || !source.name) throw new Error("Backup data pattern lacks identity");
9960
+ const retired = (record) => RETIRED_PATTERN.has(record.status ?? "active");
9961
+ if (source.type === "predefined") {
9962
+ const match2 = matchByName(destination.patterns, source.name, retired, "data pattern");
9963
+ if (!match2?.id || match2.type !== "predefined")
9964
+ throw new Error(`Missing predefined destination data pattern: ${source.name}`);
9965
+ patternPlans.push({
9966
+ sourceId: source.id,
9967
+ name: source.name,
9968
+ action: "resolve",
9969
+ existing: structuredClone(match2)
9970
+ });
9971
+ continue;
9972
+ }
9973
+ if (Object.hasOwn(patternMap2, source.name)) {
9974
+ const target = patternMap2[source.name];
9975
+ const match2 = matchByName(destination.patterns, target, retired, "data pattern");
9976
+ if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
9977
+ patternPlans.push({
9978
+ sourceId: source.id,
9979
+ name: target,
9980
+ action: "map",
9981
+ existing: structuredClone(match2)
9982
+ });
9983
+ continue;
9984
+ }
9985
+ if (!portablePattern(source))
9986
+ throw new Error(
9987
+ `Data pattern uses a tenant-bound detection technique: ${source.name}; provision it in the destination and bind it with --pattern-map "${source.name}=<destination-name>"`
9988
+ );
9989
+ const name = `${prefix}${source.name}`;
9990
+ assertName(name);
9991
+ const body = patternRequest(source, name);
9992
+ if (crossTenant) assertKnownReferences(body, /* @__PURE__ */ new Set(), `pattern ${source.name}`);
9993
+ const match = matchByName(destination.patterns, name, retired, "data pattern");
9994
+ if (!match) {
9995
+ patternPlans.push({ sourceId: source.id, name, action: "create", body });
9996
+ continue;
9997
+ }
9998
+ if (!match.id) throw new Error(`Destination data pattern has no identity: ${name}`);
9999
+ if (!compareEcho(body, match).matches)
10000
+ throw new Error(
10001
+ `Destination data pattern differs: ${name}; use --name-prefix to avoid changing a shared pattern`
10002
+ );
10003
+ patternPlans.push({
10004
+ sourceId: source.id,
10005
+ name,
10006
+ action: "reuse",
10007
+ body,
10008
+ existing: structuredClone(match)
10009
+ });
10010
+ }
10011
+ const profilePlans = [];
10012
+ for (const source of backup.profiles) {
10013
+ if (!source.name) throw new Error("Backup data profile lacks identity");
10014
+ if (source.type === "predefined") throw new Error("Backup contains a predefined data profile");
10015
+ const name = `${prefix}${source.name}`;
10016
+ assertName(name);
10017
+ const match = matchByName(
10018
+ destination.profiles,
10019
+ name,
10020
+ (record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
10021
+ "data profile"
10022
+ );
10023
+ if (match && conflict === "error")
10024
+ throw new Error(
10025
+ `Destination data profile already exists: ${name}; choose --on-conflict verify or skip`
10026
+ );
10027
+ if (match && !match.id) throw new Error(`Destination data profile has no identity: ${name}`);
10028
+ profilePlans.push({
10029
+ name,
10030
+ source: structuredClone(source),
10031
+ existing: match ? structuredClone(match) : void 0,
10032
+ action: match ? conflict === "skip" ? "skip" : "verify" : "create"
10033
+ });
10034
+ }
10035
+ const plan = {
10036
+ sourceTsgId: backup.source.tsgId,
10037
+ destinationTsgId,
10038
+ dictionaries: dictionaryPlans,
10039
+ patterns: patternPlans,
10040
+ profiles: profilePlans,
10041
+ maxPages: maxPages3
10042
+ };
10043
+ const bindings = planBindings(plan);
10044
+ const planned = /* @__PURE__ */ new Set([
10045
+ ...dictionaryPlans.map((d) => d.sourceId),
10046
+ ...patternPlans.map((p) => p.sourceId)
10047
+ ]);
10048
+ for (const item of profilePlans) {
10049
+ if (item.action === "skip") continue;
10050
+ for (const leaf of ruleLeaves(item.source)) {
10051
+ const id = leaf.value.id;
10052
+ if (id != null && id !== "" && !planned.has(String(id)))
10053
+ throw new Error(
10054
+ `Backup data profile references a resource absent from the backup: ${item.name}`
10055
+ );
10056
+ }
10057
+ const body = profileRequest(item, bindings, crossTenant, item.action === "verify");
10058
+ if (item.action === "verify" && body && !compareEcho(body, item.existing).matches)
10059
+ throw new Error(
10060
+ `Destination data profile does not match restore plan: ${item.name}; verification made no changes`
10061
+ );
10062
+ }
10063
+ return plan;
10064
+ }
10065
+ function planBindings(plan) {
10066
+ const bindings = /* @__PURE__ */ new Map();
10067
+ for (const item of plan.dictionaries)
10068
+ if (item.existing?.id) bindings.set(item.sourceId, { id: item.existing.id, name: item.name });
10069
+ for (const item of plan.patterns)
10070
+ if (item.existing?.id)
10071
+ bindings.set(item.sourceId, {
10072
+ id: item.existing.id,
10073
+ name: item.name,
10074
+ version: item.existing.version ?? void 0
10075
+ });
10076
+ return bindings;
10077
+ }
10078
+ function profileRequest(item, bindings, crossTenant, requireBindings) {
10079
+ const detectionRules = structuredClone(item.source.detection_rules ?? []);
10080
+ const known = /* @__PURE__ */ new Set();
10081
+ let deferred = false;
10082
+ const remapped = { ...structuredClone(item.source), detection_rules: detectionRules };
10083
+ for (const leaf of ruleLeaves(remapped)) {
10084
+ known.add(leaf.value);
10085
+ const id = leaf.value.id;
10086
+ if (id == null || id === "") continue;
10087
+ const binding = bindings.get(String(id));
10088
+ if (!binding) {
10089
+ if (requireBindings)
10090
+ throw new Error(
10091
+ `Missing planned dependency binding; an existing data profile cannot be verified without its destination dependencies: ${item.name}`
10092
+ );
10093
+ deferred = true;
10094
+ continue;
10095
+ }
10096
+ leaf.value.id = binding.id;
10097
+ if (leaf.value.name != null) leaf.value.name = binding.name;
10098
+ if (leaf.value.version != null) {
10099
+ if (binding.version === void 0)
10100
+ throw new Error(`Destination data pattern version is unknown: ${binding.name}`);
10101
+ leaf.value.version = binding.version;
10102
+ }
10103
+ }
10104
+ const body = {
10105
+ name: item.name,
10106
+ detection_rules: detectionRules,
10107
+ ...item.source.profile_type != null ? { profile_type: item.source.profile_type } : {},
10108
+ ...item.source.description != null ? { description: item.source.description } : {},
10109
+ ...item.source.is_granular_data_profile != null ? { is_granular_data_profile: item.source.is_granular_data_profile } : {}
10110
+ };
10111
+ if (crossTenant) assertKnownReferences(body, known, `profile ${item.name}`);
10112
+ const parsed = AdvancedDataProfileRequestSchema.parse(body);
10113
+ return deferred ? void 0 : parsed;
10114
+ }
10115
+ var DEPENDENCY_ACTIONS = {
10116
+ create: "created",
10117
+ reuse: "reused",
10118
+ resolve: "resolved",
10119
+ map: "mapped"
10120
+ };
10121
+ function assertUnchanged(current, prior, label) {
10122
+ if (canonical(current ?? null) !== canonical(prior ?? null))
10123
+ throw new Error(`Destination ${label} changed after planning`);
10124
+ }
10125
+ async function restoreDlpResources(api, plan) {
10126
+ const result = {
10127
+ sourceTsgId: plan.sourceTsgId,
10128
+ destinationTsgId: plan.destinationTsgId,
10129
+ complete: false,
10130
+ dictionaries: [],
10131
+ patterns: [],
10132
+ profiles: [],
10133
+ serverAdded: []
10134
+ };
10135
+ const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
10136
+ try {
10137
+ const current = await inventories(api, plan.maxPages);
10138
+ for (const item of plan.dictionaries) {
10139
+ const match = matchByName(current.dictionaries, item.name, () => false, "dictionary");
10140
+ if (item.existing) assertUnchanged(match, item.existing, `dictionary: ${item.name}`);
10141
+ else if (match)
10142
+ throw new Error(`Destination dictionary changed after planning: ${item.name}`);
10143
+ }
10144
+ const retiredPattern = (record) => RETIRED_PATTERN.has(record.status ?? "active");
10145
+ for (const item of plan.patterns) {
10146
+ const match = matchByName(current.patterns, item.name, retiredPattern, "data pattern");
10147
+ if (item.existing) assertUnchanged(match, item.existing, `data pattern: ${item.name}`);
10148
+ else if (match)
10149
+ throw new Error(`Destination data pattern changed after planning: ${item.name}`);
10150
+ }
10151
+ for (const item of plan.profiles) {
10152
+ const match = matchByName(
10153
+ current.profiles,
10154
+ item.name,
10155
+ (record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
10156
+ "data profile"
10157
+ );
10158
+ if (item.existing) assertUnchanged(match, item.existing, `data profile: ${item.name}`);
10159
+ else if (match)
10160
+ throw new Error(`Destination data profile changed after planning: ${item.name}`);
10161
+ }
10162
+ const bindings = planBindings(plan);
10163
+ for (const item of plan.dictionaries) {
10164
+ if (item.action !== "create") {
10165
+ const id = item.existing?.id;
10166
+ if (!id) throw new Error(`Destination dictionary has no identity: ${item.name}`);
10167
+ result.dictionaries.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
10168
+ continue;
10169
+ }
10170
+ if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
10171
+ const created = await api.dictionaries.create({
10172
+ metadata: item.metadata,
10173
+ file: `${item.keywords.join("\n")}
10174
+ `,
10175
+ includeKeywords: true
10176
+ });
10177
+ if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
10178
+ const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
10179
+ const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
10180
+ if (!echo.matches || readBack.name !== item.name)
10181
+ throw new Error(`Restored dictionary did not verify: ${item.name}`);
10182
+ if (echo.serverAdded.length)
10183
+ result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
10184
+ bindings.set(item.sourceId, { id: created.id, name: item.name });
10185
+ result.dictionaries.push({ name: item.name, action: "created", id: created.id });
10186
+ }
10187
+ for (const item of plan.patterns) {
10188
+ if (item.action !== "create") {
10189
+ const id = item.existing?.id;
10190
+ if (!id) throw new Error(`Destination data pattern has no identity: ${item.name}`);
10191
+ result.patterns.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
10192
+ continue;
10193
+ }
10194
+ if (!item.body) throw new Error("Data pattern plan is incomplete");
10195
+ const created = await api.patterns.create(item.body);
10196
+ if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
10197
+ const readBack = await api.patterns.get(created.id);
10198
+ const echo = compareEcho(item.body, readBack);
10199
+ if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10200
+ throw new Error(`Restored data pattern did not verify: ${item.name}`);
10201
+ if (echo.serverAdded.length)
10202
+ result.serverAdded.push({
10203
+ resource: `data pattern ${item.name}`,
10204
+ fields: echo.serverAdded
10205
+ });
10206
+ bindings.set(item.sourceId, {
10207
+ id: created.id,
10208
+ name: item.name,
10209
+ version: readBack.version ?? void 0
10210
+ });
10211
+ result.patterns.push({ name: item.name, action: "created", id: created.id });
10212
+ }
10213
+ for (const item of plan.profiles) {
10214
+ if (item.action === "skip") {
10215
+ result.profiles.push({
10216
+ name: item.name,
10217
+ action: "skipped",
10218
+ id: item.existing?.id ?? void 0
10219
+ });
10220
+ continue;
10221
+ }
10222
+ const body = profileRequest(item, bindings, crossTenant, true);
10223
+ if (!body) throw new Error(`Missing planned dependency binding: ${item.name}`);
10224
+ if (item.action === "verify") {
10225
+ const echo2 = compareEcho(body, item.existing);
10226
+ if (!echo2.matches)
10227
+ throw new Error(`Destination data profile does not match restore plan: ${item.name}`);
10228
+ if (echo2.serverAdded.length)
10229
+ result.serverAdded.push({
10230
+ resource: `data profile ${item.name}`,
10231
+ fields: echo2.serverAdded
10232
+ });
10233
+ result.profiles.push({
10234
+ name: item.name,
10235
+ action: "verified",
10236
+ id: item.existing?.id ?? void 0
10237
+ });
10238
+ continue;
10239
+ }
10240
+ const created = await api.profiles.create(body);
10241
+ if (!created.id) throw new Error(`Restored data profile did not verify: ${item.name}`);
10242
+ const readBack = await api.profiles.get(created.id);
10243
+ const echo = compareEcho(body, readBack);
10244
+ if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10245
+ throw new Error(`Restored data profile did not verify: ${item.name}`);
10246
+ if (echo.serverAdded.length)
10247
+ result.serverAdded.push({
10248
+ resource: `data profile ${item.name}`,
10249
+ fields: echo.serverAdded
10250
+ });
10251
+ result.profiles.push({ name: item.name, action: "created", id: created.id });
10252
+ }
10253
+ result.complete = true;
10254
+ } catch (error) {
10255
+ const status = error?.statusCode;
10256
+ result.error = Number.isInteger(status) ? `API request failed (HTTP ${status}); verify destination state before retrying` : error instanceof Error && /^(Destination|Restored|Missing|Inventory|Ambiguous|Dictionary|Data pattern|Data profile)/.test(
10257
+ error.message
10258
+ ) ? error.message : "Restore stopped; verify destination state before retrying";
10259
+ }
10260
+ return result;
10261
+ }
10262
+
10263
+ // src/cli/commands/dlp/transfer.ts
10264
+ var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
10265
+ var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
10266
+ function maxPages(value) {
10267
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
10268
+ usageError("--max-pages must be an integer from 1 to 1000");
10269
+ return Number(value);
10270
+ }
10271
+ function resourceKinds(value) {
10272
+ const kinds = value.split(",").map((kind) => kind.trim());
10273
+ if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)) || new Set(kinds).size !== kinds.length)
10274
+ throw new CliUsageError("--resources takes a unique list of dictionaries, patterns, profiles");
10275
+ return kinds;
10276
+ }
10277
+ function patternMap(values) {
10278
+ const entries = values.map((value) => {
10279
+ const at = value.indexOf("=");
10280
+ if (at < 1 || at === value.length - 1)
10281
+ throw new CliUsageError("--pattern-map requires source-name=destination-name");
10282
+ return [value.slice(0, at), value.slice(at + 1)];
10283
+ });
10284
+ if (new Set(entries.map(([key]) => key)).size !== entries.length)
10285
+ throw new CliUsageError("Duplicate source names in --pattern-map");
10286
+ return Object.fromEntries(entries);
10287
+ }
10288
+ async function context() {
10289
+ const config = await loadConfig();
10290
+ if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
10291
+ throw new CliUsageError(
10292
+ "DLP transfer requires configured Management client ID, secret and TSG ID"
10293
+ );
10294
+ const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
10295
+ return {
10296
+ api: {
10297
+ dictionaries: client.dlp.dictionaries,
10298
+ patterns: client.dlp.dataPatterns,
10299
+ profiles: client.dlp.dataProfiles
10300
+ },
10301
+ tsgId: config.mgmtTsgId
10302
+ };
10303
+ }
10304
+ function failure(error) {
10305
+ if (error instanceof CliUsageError) usageError(error.message);
10306
+ if (error instanceof ZodError) {
10307
+ ui.error("Invalid DLP backup/request schema; content is not logged.");
10308
+ } else if (error instanceof AISecSDKException2) {
10309
+ ui.error(
10310
+ `DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
10311
+ );
10312
+ } else if (error?.code) {
10313
+ ui.error(
10314
+ `DLP transfer file operation failed (${error.code}); existing files are not overwritten.`
10315
+ );
10316
+ } else {
10317
+ ui.error(error instanceof Error ? error.message : "DLP transfer failed");
10318
+ }
10319
+ process.exitCode = 1;
10320
+ }
10321
+ function renderRestore(value, columns, format) {
10322
+ if (format !== "pretty" && format !== "table" && format !== "markdown")
10323
+ return formatOutput([value], columns, format);
10324
+ const markdown = format === "markdown";
10325
+ const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
10326
+ const heading = (text2) => markdown ? `### ${text2}
10327
+ ` : text2;
10328
+ const blocks = [
10329
+ heading(
10330
+ value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
10331
+ ),
10332
+ `Source TSG: ${value.sourceTsgId}
10333
+ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10334
+ ];
10335
+ for (const [key, title] of [
10336
+ ["dictionaries", "Dictionaries"],
10337
+ ["patterns", "Data patterns"],
10338
+ ["profiles", "Data profiles"]
10339
+ ]) {
10340
+ blocks.push(heading(`${title} (${rows(key).length})`));
10341
+ blocks.push(
10342
+ formatOutput(
10343
+ rows(key),
10344
+ [
10345
+ { key: "action", label: "Action" },
10346
+ { key: "name", label: "Name" }
10347
+ ],
10348
+ markdown ? "markdown" : "table"
10349
+ ) || "(none)"
10350
+ );
10351
+ }
10352
+ if (rows("serverAdded").length) {
10353
+ blocks.push(heading("Verified server-added fields (source omitted these)"));
10354
+ for (const item of rows("serverAdded"))
10355
+ blocks.push(
10356
+ `${item.resource}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
10357
+ );
10358
+ }
10359
+ if (value.error) blocks.push(`Error: ${value.error}`);
10360
+ return blocks.join("\n\n");
10361
+ }
10362
+ function register6(dlp) {
10363
+ const backup = dlp.command("backup").description(
10364
+ "Back up custom DLP dictionaries (with keywords), data patterns and data profiles with their referenced dependencies"
10365
+ ).option(
10366
+ "--resources <list>",
10367
+ "Comma-separated primary resources: dictionaries, patterns, profiles (default: all)",
10368
+ resourceKinds
10369
+ ).option("--file-format <format>", "Backup file format: json or yaml", "json").option("--output-file <path>", "New private backup file; defaults to the current directory").option(
10370
+ "--skip-unsupported",
10371
+ "Exclude profiles with non-transferable rules (multi-profile, direct EDM) instead of failing"
10372
+ ).option(
10373
+ "--max-pages <n>",
10374
+ "Maximum pages per inventory; incomplete exports fail",
10375
+ maxPages,
10376
+ 100
10377
+ ).option("--output <format>", FORMATS).addHelpText(
10378
+ "after",
10379
+ examples(
10380
+ "airs runtime dlp backup --output-file ./dlp-backup.json",
10381
+ "airs runtime dlp backup --resources profiles --skip-unsupported --output-file ./profiles.yaml --file-format yaml"
10382
+ )
10383
+ ).action(async (opts) => {
10384
+ try {
10385
+ if (!["json", "yaml"].includes(opts.fileFormat))
10386
+ throw new CliUsageError("--file-format must be json or yaml");
10387
+ const format = await resolveOutput(backup, opts);
10388
+ const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
10389
+ if (opts.outputFile === "-")
10390
+ throw new CliUsageError(
10391
+ "Backups contain dictionary keywords and require a private file destination, not stdout"
10392
+ );
10393
+ const path3 = resolve5(
10394
+ opts.outputFile ?? `airs-dlp-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
10395
+ );
10396
+ if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
10397
+ extname(path3).toLowerCase()
10398
+ ))
10399
+ throw new CliUsageError("Backup filename extension must match --file-format");
10400
+ const { api, tsgId } = await context();
10401
+ const { backup: data, skipped } = await backupDlpResources(api, tsgId, {
10402
+ resources: opts.resources,
10403
+ maxPages: opts.maxPages,
10404
+ skipUnsupported: Boolean(opts.skipUnsupported)
10405
+ });
10406
+ for (const item of skipped)
10407
+ ui.warning(`Skipped unsupported data profile: ${item.profile} (${item.reason})`);
10408
+ const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
10409
+ `;
10410
+ if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
10411
+ throw new Error("Backup exceeds the 20 MiB size limit; select fewer --resources");
10412
+ await writeReportFile(path3, content);
10413
+ console.log(
10414
+ formatOutput(
10415
+ [
10416
+ {
10417
+ file: path3,
10418
+ sourceTsgId: tsgId,
10419
+ dictionaries: data.dictionaries.length,
10420
+ patterns: data.patterns.length,
10421
+ profiles: data.profiles.length,
10422
+ skipped: skipped.length
10423
+ }
10424
+ ],
10425
+ [
10426
+ { key: "file", label: "Backup file" },
10427
+ { key: "sourceTsgId", label: "Source TSG" },
10428
+ { key: "dictionaries", label: "Dictionaries" },
10429
+ { key: "patterns", label: "Patterns" },
10430
+ { key: "profiles", label: "Profiles" },
10431
+ { key: "skipped", label: "Skipped" }
10432
+ ],
10433
+ format === "pretty" ? "table" : format
10434
+ )
10435
+ );
10436
+ } catch (error) {
10437
+ failure(error);
10438
+ }
10439
+ });
10440
+ const restore = dlp.command("restore <file>").description(
10441
+ "Restore DLP resources into the selected tenant, staged dictionaries \u2192 patterns \u2192 profiles; validate all dependencies before changes"
10442
+ ).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored custom resource names to avoid collisions").option(
10443
+ "--on-conflict <policy>",
10444
+ "Existing data profile names: error, verify (resume without writes) or skip. Updates are unsupported until the DLP profile write path is live-verified.",
10445
+ "error"
10446
+ ).option(
10447
+ "--pattern-map <source=destination>",
10448
+ "Bind a tenant-bound source pattern to an existing destination pattern (repeatable)",
10449
+ (value, prior) => [...prior, value],
10450
+ []
10451
+ ).option(
10452
+ "--max-pages <n>",
10453
+ "Maximum pages per inventory; incomplete inventories fail",
10454
+ maxPages,
10455
+ 100
10456
+ ).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
10457
+ "--force",
10458
+ "Execute without a prompt; requires --expect-tsg and does not override conflicts"
10459
+ ).option("--output <format>", FORMATS).addHelpText(
10460
+ "after",
10461
+ examples(
10462
+ "airs runtime dlp restore ./dlp-backup.json --dry-run --output json",
10463
+ 'airs runtime dlp restore ./dlp-backup.json --pattern-map "EDM SSN=Dest SSN" --dry-run',
10464
+ "airs runtime dlp restore ./dlp-backup.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
10465
+ )
10466
+ ).action(async (file, opts) => {
10467
+ try {
10468
+ if (!["error", "verify", "skip"].includes(opts.onConflict))
10469
+ throw new CliUsageError("--on-conflict must be error, verify or skip");
10470
+ if (opts.force && !opts.expectTsg)
10471
+ throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
10472
+ const mappings = patternMap(opts.patternMap);
10473
+ const path3 = resolve5(file);
10474
+ const extension = extname(path3).toLowerCase();
10475
+ if (![".json", ".yaml", ".yml"].includes(extension))
10476
+ throw new CliUsageError("Backup file must use .json, .yaml or .yml");
10477
+ const info = await stat(path3);
10478
+ if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
10479
+ throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
10480
+ let input2;
10481
+ try {
10482
+ const text2 = await readFile9(path3, "utf8");
10483
+ if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
10484
+ input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
10485
+ } catch {
10486
+ throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
10487
+ }
10488
+ const data = DlpResourcesBackupSchema.parse(input2);
10489
+ const format = await resolveOutput(restore, opts);
10490
+ const { api, tsgId } = await context();
10491
+ if (opts.expectTsg && opts.expectTsg !== tsgId)
10492
+ throw new CliUsageError(
10493
+ "--expect-tsg does not match the selected destination tenant; no API request was made"
10494
+ );
10495
+ const plan = await planDlpResourcesRestore(api, data, tsgId, {
10496
+ namePrefix: opts.namePrefix,
10497
+ onConflict: opts.onConflict,
10498
+ patternMap: mappings,
10499
+ maxPages: opts.maxPages
10500
+ });
10501
+ const summary = {
10502
+ sourceTsgId: plan.sourceTsgId,
10503
+ destinationTsgId: plan.destinationTsgId,
10504
+ dryRun: Boolean(opts.dryRun),
10505
+ dictionaries: plan.dictionaries.map((d) => ({ name: d.name, action: d.action })),
10506
+ patterns: plan.patterns.map((p) => ({ name: p.name, action: p.action })),
10507
+ profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action }))
10508
+ };
10509
+ const columns = [
10510
+ { key: "sourceTsgId", label: "Source TSG" },
10511
+ { key: "destinationTsgId", label: "Destination TSG" },
10512
+ { key: "dictionaries", label: "Dictionaries" },
10513
+ { key: "patterns", label: "Data patterns" },
10514
+ { key: "profiles", label: "Data profiles" },
10515
+ { key: "serverAdded", label: "Verified server-added fields" },
10516
+ { key: "dryRun", label: "Dry run" },
10517
+ { key: "complete", label: "Complete" }
10518
+ ];
10519
+ if (opts.dryRun) {
10520
+ console.log(renderRestore(summary, columns, format));
10521
+ return;
10522
+ }
10523
+ const writes = plan.dictionaries.filter((d) => d.action === "create").length + plan.patterns.filter((p) => p.action === "create").length + plan.profiles.filter((p) => p.action === "create").length;
10524
+ if (writes)
10525
+ await confirmOrAbort(
10526
+ `Restore ${writes} DLP resources from TSG ${plan.sourceTsgId} to TSG ${tsgId}?`,
10527
+ Boolean(opts.force),
10528
+ { action: `restore DLP resources into TSG ${tsgId}` }
10529
+ );
10530
+ const result = await restoreDlpResources(api, plan);
10531
+ console.log(renderRestore({ ...result }, columns, format));
10532
+ if (!result.complete) {
10533
+ ui.error(result.error ?? "Restore incomplete");
10534
+ process.exitCode = 1;
10535
+ }
10536
+ } catch (error) {
10537
+ failure(error);
10538
+ }
10539
+ });
10540
+ }
10541
+
10542
+ // src/cli/commands/dlp/index.ts
10543
+ function registerDlpCommands(runtime) {
10544
+ const dlp = runtime.command("dlp").description(
10545
+ "DLP management (filtering-profiles, patterns, profiles, dictionaries, generate, backup/restore)"
10546
+ );
10547
+ register2(dlp);
10548
+ register4(dlp);
10549
+ register5(dlp);
10550
+ register(dlp);
10551
+ register3(dlp);
10552
+ register6(dlp);
10553
+ }
10554
+
10555
+ // src/cli/commands/profile-transfer.ts
10556
+ import { randomUUID as randomUUID8 } from "crypto";
10557
+ import { readFile as readFile10, stat as stat2 } from "fs/promises";
10558
+ import { extname as extname2, resolve as resolve6 } from "path";
10559
+ import { AISecSDKException as AISecSDKException3, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
10560
+ import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
10561
+ import { ZodError as ZodError2 } from "zod";
10562
+
10563
+ // src/backup/runtime-profiles.ts
10564
+ import {
10565
+ CreateSecurityProfileRequestSchema,
10566
+ CustomTopicSchema,
10567
+ PolicySchema,
10568
+ SecurityProfileSchema
10569
+ } from "@cdot65/prisma-airs-sdk";
10570
+ import { z as z6 } from "zod";
10571
+
10572
+ // src/backup/runtime-policy.ts
10573
+ function object2(value) {
10574
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10575
+ }
10576
+ function compareRuntimePolicies(expected, actual) {
10577
+ const serverDefaults = [];
10578
+ const differences = [];
10579
+ const visit = (source, target, path3, insideToxicCategories = false, insideTopicGuardrails = false) => {
10580
+ if (source === target) return;
10581
+ if (Array.isArray(source) && Array.isArray(target)) {
10582
+ if (source.length !== target.length) differences.push(`${path3}.length`);
10583
+ source.forEach((item, index) => {
10584
+ visit(
10585
+ item,
10586
+ target[index],
10587
+ `${path3}[${index}]`,
10588
+ insideToxicCategories,
10589
+ insideTopicGuardrails
10590
+ );
10591
+ });
10592
+ return;
10593
+ }
10594
+ if (object2(source) && object2(target)) {
10595
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
10596
+ const field = `${path3}.${key}`;
10597
+ if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
10598
+ const model = /^policy\.ai-security-profiles\[\d+\]\.model-configuration/;
10599
+ const relative = path3.replace(model, "model");
10600
+ let allowed;
10601
+ if (relative === "model.data-protection" && key === "database-security") allowed = null;
10602
+ if (key === "severity") {
10603
+ if (/^model\.data-protection\.database-security\[\d+\]$/.test(relative)) {
10604
+ const values = {
10605
+ "database-security-create": "medium",
10606
+ "database-security-read": "low",
10607
+ "database-security-update": "medium",
10608
+ "database-security-delete": "high"
10609
+ };
10610
+ if (source.name === target.name && typeof source.name === "string")
10611
+ allowed = Object.hasOwn(values, source.name) ? values[source.name] : void 0;
10612
+ }
10613
+ if (relative === "model.data-protection.source-code-detection") allowed = "high";
10614
+ if (relative === "model.app-protection.malicious-code-protection" && source.name === "malicious-code" && target.name === source.name)
10615
+ allowed = "high";
10616
+ 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)
10617
+ allowed = "medium";
10618
+ 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)
10619
+ allowed = "medium";
10620
+ if (/^model\.agent-protection\[\d+\]$/.test(relative) && source.name === "agent-security" && target.name === source.name)
10621
+ allowed = "medium";
10622
+ }
10623
+ if (relative === "model.app-protection" && key === "url-detected-severity")
10624
+ allowed = "low";
10625
+ if (relative === "model.app-protection" && key === "default-url-category" && object2(target[key])) {
10626
+ const value = target[key];
10627
+ if (Object.keys(value).length === 1 && value.member === null) {
10628
+ serverDefaults.push(field);
10629
+ continue;
10630
+ }
10631
+ }
10632
+ 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" && object2(target[key])) {
10633
+ const value = target[key];
10634
+ if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
10635
+ serverDefaults.push(field);
10636
+ continue;
10637
+ }
10638
+ }
10639
+ if (allowed !== void 0 && target[key] === allowed) {
10640
+ serverDefaults.push(field);
10641
+ continue;
10642
+ }
10643
+ differences.push(field);
10644
+ } else if (!Object.hasOwn(target, key)) differences.push(field);
10645
+ else
10646
+ visit(
10647
+ source[key],
10648
+ target[key],
10649
+ field,
10650
+ key === "toxic-category-list" && source.name === "toxic-content" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
10651
+ path3
10652
+ ),
10653
+ key === "topic-list" && source.name === "topic-guardrails" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
10654
+ path3
10655
+ ) || insideTopicGuardrails && key === "topic" && source.action === "block" && target.action === "block"
10656
+ );
10657
+ }
10658
+ return;
10659
+ }
10660
+ differences.push(path3);
10661
+ };
10662
+ visit(expected, actual, "policy");
10663
+ return { matches: differences.length === 0, serverDefaults, differences };
10664
+ }
10665
+
10666
+ // src/backup/runtime-profiles.ts
10667
+ var RuntimeProfilesBackupSchema = z6.object({
10668
+ version: z6.literal(1),
10669
+ resourceType: z6.literal("runtime-security-profiles"),
10670
+ exportedAt: z6.string().datetime({ offset: true }),
10671
+ source: z6.object({ tsgId: z6.string().min(1) }).strict(),
10672
+ profiles: z6.array(SecurityProfileSchema).min(1).max(1e4),
10673
+ topics: z6.array(CustomTopicSchema).max(1e4)
10674
+ }).strict();
10675
+ function object3(value) {
10676
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10677
+ }
10678
+ function pagesLimit2(value) {
10679
+ if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
10680
+ throw new Error("maxPages must be an integer from 1 to 1000");
10681
+ }
10682
+ async function offsetPages(fetch, maxPages3) {
10683
+ pagesLimit2(maxPages3);
10684
+ const items = [];
10685
+ let offset = 0;
10686
+ for (let page = 0; page < maxPages3; page++) {
10687
+ const result = await fetch(offset);
10688
+ items.push(...result.items);
10689
+ if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
10690
+ if (result.next === void 0 || result.next === 0) return items;
10691
+ if (!result.items.length || !Number.isSafeInteger(result.next) || result.next <= offset)
10692
+ throw new Error("Inventory pagination did not advance");
10693
+ offset = result.next;
10694
+ }
10695
+ throw new Error("Inventory is incomplete: increase --max-pages before exporting or restoring");
10696
+ }
10697
+ function latestProfiles(profiles2) {
10698
+ const names = /* @__PURE__ */ new Map();
10699
+ const revisions = /* @__PURE__ */ new Set();
10700
+ for (const profile of profiles2) {
10701
+ assertName2(profile.profile_name);
10702
+ const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
10703
+ if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
10704
+ revisions.add(identity2);
10705
+ const prior = names.get(profile.profile_name);
10706
+ if (prior && prior.profile_id !== profile.profile_id)
10707
+ throw new Error(`Ambiguous profile name: ${profile.profile_name}`);
10708
+ if (!prior || (profile.revision ?? 0) > (prior.revision ?? 0))
10709
+ names.set(profile.profile_name, profile);
10710
+ }
10711
+ return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
10712
+ }
10713
+ function assertName2(name) {
10714
+ if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
10715
+ throw new Error("Profile/topic names must be nonempty and contain no control characters");
10716
+ }
10717
+ function assertTenant(profiles2, tsgId) {
10718
+ if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
10719
+ throw new Error("Destination/source profile tenant identity does not match the selected TSG");
10720
+ }
10721
+ async function profiles(api, maxPages3) {
10722
+ return latestProfiles(
10723
+ await offsetPages(async (offset) => {
10724
+ const page = await api.profiles.list({ offset, limit: 100, latest: true });
10725
+ return { items: page.ai_profiles, next: page.next_offset };
10726
+ }, maxPages3)
10727
+ );
10728
+ }
10729
+ async function topics(api, maxPages3) {
10730
+ return offsetPages(async (offset) => {
10731
+ const page = await api.topics.list({ offset, limit: 100 });
10732
+ return { items: page.custom_topics, next: page.next_offset };
10733
+ }, maxPages3);
10734
+ }
10735
+ function topicReferences(policy) {
10736
+ const refs = [];
10737
+ const visit = (value) => {
10738
+ if (Array.isArray(value)) {
10739
+ value.forEach(visit);
10740
+ return;
10741
+ }
10742
+ if (!object3(value)) return;
10743
+ if ("topic_id" in value) {
10744
+ if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
10745
+ throw new Error("Invalid topic identity in profile policy");
10746
+ refs.push(value);
10747
+ }
10748
+ Object.values(value).forEach(visit);
10749
+ };
10750
+ visit(policy);
10751
+ return refs;
10752
+ }
10753
+ function topicKey(ref) {
10754
+ return JSON.stringify([ref.topic_id, ref.revision]);
10755
+ }
10756
+ async function backupRuntimeProfiles(api, tsgId, options = {}) {
10757
+ if (!tsgId) throw new Error("Source TSG ID is required");
10758
+ const maxPages3 = options.maxPages ?? 100;
10759
+ let selected = await profiles(api, maxPages3);
10760
+ assertTenant(selected, tsgId);
10761
+ if (options.profile)
10762
+ selected = selected.filter(
10763
+ (p) => p.profile_id === options.profile || p.profile_name === options.profile
10764
+ );
10765
+ if (!selected.length) throw new Error("No matching profiles to back up");
10766
+ if (options.profile && selected.length !== 1) throw new Error("Ambiguous profile selector");
10767
+ for (const profile of selected) {
10768
+ if (!profile.policy) throw new Error(`Profile has no policy: ${profile.profile_name}`);
10769
+ PolicySchema.parse(profile.policy);
10770
+ }
10771
+ const refs = selected.flatMap((profile) => topicReferences(profile.policy));
10772
+ const definitions = refs.length ? await topics(api, maxPages3) : [];
10773
+ const needed = /* @__PURE__ */ new Map();
10774
+ for (const ref of refs) {
10775
+ const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
10776
+ if (matches.length !== 1 || matches[0].topic_name !== ref.topic_name)
10777
+ throw new Error(
10778
+ `Cannot export the exact referenced topic revision: ${ref.topic_name} revision ${ref.revision}`
9766
10779
  );
9767
10780
  needed.set(topicKey(ref), matches[0]);
9768
10781
  }
@@ -9779,21 +10792,21 @@ function sameTopic(a, b) {
9779
10792
  return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
9780
10793
  }
9781
10794
  function dlpReferences(policy) {
9782
- if (!object2(policy)) return [];
10795
+ if (!object3(policy)) return [];
9783
10796
  const refs = [];
9784
10797
  for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
9785
- if (!object2(entry) || typeof entry.name !== "string")
10798
+ if (!object3(entry) || typeof entry.name !== "string")
9786
10799
  throw new Error("Invalid embedded DLP reference");
9787
10800
  refs.push({ value: entry, name: entry.name, kind: "embedded" });
9788
10801
  }
9789
10802
  for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
9790
- if (!object2(entry) || !object2(entry["model-configuration"])) continue;
10803
+ if (!object3(entry) || !object3(entry["model-configuration"])) continue;
9791
10804
  const protection = entry["model-configuration"]["data-protection"];
9792
- if (!object2(protection) || !object2(protection["data-leak-detection"])) continue;
10805
+ if (!object3(protection) || !object3(protection["data-leak-detection"])) continue;
9793
10806
  const members = protection["data-leak-detection"].member;
9794
10807
  if (!Array.isArray(members)) continue;
9795
10808
  for (const member of members) {
9796
- if (!object2(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
10809
+ if (!object3(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
9797
10810
  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))
9798
10811
  continue;
9799
10812
  refs.push({ value: member, name: member.text, kind: "member" });
@@ -9836,7 +10849,7 @@ function assertKnownPolicyReferences(policy) {
9836
10849
  });
9837
10850
  return;
9838
10851
  }
9839
- if (!object2(value)) return;
10852
+ if (!object3(value)) return;
9840
10853
  for (const [key, child] of Object.entries(value)) {
9841
10854
  if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
9842
10855
  throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
@@ -9845,9 +10858,9 @@ function assertKnownPolicyReferences(policy) {
9845
10858
  };
9846
10859
  visit(policy, "policy");
9847
10860
  }
9848
- async function dataProfiles(api, maxPages2) {
10861
+ async function dataProfiles(api, maxPages3) {
9849
10862
  const items = [];
9850
- for (let page = 0; page < maxPages2; page++) {
10863
+ for (let page = 0; page < maxPages3; page++) {
9851
10864
  const result = await api.dataProfiles.list({ page, size: 100 });
9852
10865
  items.push(...result.content);
9853
10866
  if (items.length > 1e4) throw new Error("DLP inventory exceeds the safety limit");
@@ -9860,8 +10873,8 @@ async function dataProfiles(api, maxPages2) {
9860
10873
  async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
9861
10874
  const backup = RuntimeProfilesBackupSchema.parse(input2);
9862
10875
  if (!destinationTsgId) throw new Error("Destination TSG ID is required");
9863
- const maxPages2 = options.maxPages ?? 100;
9864
- pagesLimit(maxPages2);
10876
+ const maxPages3 = options.maxPages ?? 100;
10877
+ pagesLimit2(maxPages3);
9865
10878
  const prefix = options.namePrefix ?? "";
9866
10879
  if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
9867
10880
  throw new Error("Invalid --name-prefix");
@@ -9884,7 +10897,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9884
10897
  });
9885
10898
  if (crossTenant) assertKnownPolicyReferences(profile.policy);
9886
10899
  }
9887
- const existing = await profiles(api, maxPages2);
10900
+ const existing = await profiles(api, maxPages3);
9888
10901
  assertTenant(existing, destinationTsgId);
9889
10902
  const profilePlans = selected.map((source) => {
9890
10903
  const name = `${prefix}${source.profile_name}`;
@@ -9903,7 +10916,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9903
10916
  });
9904
10917
  const changing = profilePlans.filter((p) => p.action !== "skip");
9905
10918
  const refs = changing.flatMap((p) => topicReferences(p.source.policy));
9906
- const targetTopics = refs.length ? await topics(api, maxPages2) : [];
10919
+ const targetTopics = refs.length ? await topics(api, maxPages3) : [];
9907
10920
  const topicPlans = /* @__PURE__ */ new Map();
9908
10921
  const topicNames = /* @__PURE__ */ new Map();
9909
10922
  for (const ref of refs) {
@@ -9916,7 +10929,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9916
10929
  throw new Error(`Backup lacks an unambiguous topic definition: ${ref.topic_name}`);
9917
10930
  const source = definitions[0];
9918
10931
  const name = `${prefix}${source.topic_name}`;
9919
- assertName(name);
10932
+ assertName2(name);
9920
10933
  if (topicNames.has(name) && topicNames.get(name) !== key)
9921
10934
  throw new Error(
9922
10935
  `Multiple source revisions use topic name ${name}; split the backup by profile/revision before restoring`
@@ -9943,7 +10956,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9943
10956
  );
9944
10957
  if (!targetFor(name)) unresolved.add(name);
9945
10958
  }
9946
- const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages2) : [];
10959
+ const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages3) : [];
9947
10960
  const resolved = [];
9948
10961
  for (const name of names) {
9949
10962
  const target = targetFor(name);
@@ -9994,7 +11007,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
9994
11007
  dlpMappings,
9995
11008
  dlpFallbacks,
9996
11009
  serverDefaults: [],
9997
- maxPages: maxPages2
11010
+ maxPages: maxPages3
9998
11011
  };
9999
11012
  const existingTopics = new Map(
10000
11013
  plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
@@ -10011,8 +11024,8 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
10011
11024
  }
10012
11025
  return plan;
10013
11026
  }
10014
- function canonical(value) {
10015
- const order = (v) => Array.isArray(v) ? v.map(order) : object2(v) ? Object.fromEntries(
11027
+ function canonical2(value) {
11028
+ const order = (v) => Array.isArray(v) ? v.map(order) : object3(v) ? Object.fromEntries(
10016
11029
  Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
10017
11030
  ) : v;
10018
11031
  return JSON.stringify(order(value));
@@ -10023,7 +11036,7 @@ function canonicalPolicy(policy) {
10023
11036
  const protection = entry["model-configuration"]?.["data-protection"];
10024
11037
  if (protection && !("database-security" in protection)) protection["database-security"] = null;
10025
11038
  }
10026
- return canonical(value);
11039
+ return canonical2(value);
10027
11040
  }
10028
11041
  function unchanged(current, prior) {
10029
11042
  return Boolean(
@@ -10166,9 +11179,9 @@ function mappedPolicy(item, plan, mappedTopics) {
10166
11179
  }
10167
11180
 
10168
11181
  // src/cli/commands/profile-transfer.ts
10169
- var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
10170
- var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
10171
- function renderRestore(value, columns, format) {
11182
+ var MAX_BACKUP_BYTES2 = 20 * 1024 * 1024;
11183
+ var FORMATS2 = "Output format: pretty, table, markdown, csv, json, yaml";
11184
+ function renderRestore2(value, columns, format) {
10172
11185
  if (format !== "pretty" && format !== "table" && format !== "markdown")
10173
11186
  return formatOutput([value], columns, format);
10174
11187
  const markdown = format === "markdown";
@@ -10228,7 +11241,7 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10228
11241
  if (value.error) blocks.push(`Error: ${value.error}`);
10229
11242
  return blocks.join("\n\n");
10230
11243
  }
10231
- function maxPages(value) {
11244
+ function maxPages2(value) {
10232
11245
  if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
10233
11246
  usageError("--max-pages must be an integer from 1 to 1000");
10234
11247
  return Number(value);
@@ -10244,13 +11257,13 @@ function dlpMap(values) {
10244
11257
  throw new CliUsageError("Duplicate source names in --dlp-map");
10245
11258
  return Object.fromEntries(entries);
10246
11259
  }
10247
- async function context() {
11260
+ async function context2() {
10248
11261
  const config = await loadConfig();
10249
11262
  if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
10250
11263
  throw new CliUsageError(
10251
11264
  "Profile transfer requires configured Management client ID, secret and TSG ID"
10252
11265
  );
10253
- const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
11266
+ const client = new ManagementClient2({ ...managementClientOptions(config), numRetries: 0 });
10254
11267
  return {
10255
11268
  api: {
10256
11269
  profiles: client.profiles,
@@ -10260,11 +11273,11 @@ async function context() {
10260
11273
  tsgId: config.mgmtTsgId
10261
11274
  };
10262
11275
  }
10263
- function failure(error) {
11276
+ function failure2(error) {
10264
11277
  if (error instanceof CliUsageError) usageError(error.message);
10265
- if (error instanceof ZodError) {
11278
+ if (error instanceof ZodError2) {
10266
11279
  ui.error("Invalid profile backup/request schema; content is not logged.");
10267
- } else if (error instanceof AISecSDKException2) {
11280
+ } else if (error instanceof AISecSDKException3) {
10268
11281
  ui.error(
10269
11282
  `Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
10270
11283
  );
@@ -10283,9 +11296,9 @@ function registerProfileTransferCommands(profiles2) {
10283
11296
  ).option("--all", "Back up all latest profiles (cannot be combined with a profile selector)").option("--file-format <format>", "Backup file format: json or yaml", "json").option("--output-file <path>", "New private backup file; defaults to the current directory").option(
10284
11297
  "--max-pages <n>",
10285
11298
  "Maximum pages per inventory; incomplete exports fail",
10286
- maxPages,
11299
+ maxPages2,
10287
11300
  100
10288
- ).option("--output <format>", FORMATS).addHelpText(
11301
+ ).option("--output <format>", FORMATS2).addHelpText(
10289
11302
  "after",
10290
11303
  examples(
10291
11304
  "airs runtime profiles backup --all --output-file ./profiles.json",
@@ -10301,18 +11314,18 @@ function registerProfileTransferCommands(profiles2) {
10301
11314
  const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
10302
11315
  if (opts.outputFile === "-")
10303
11316
  throw new CliUsageError("Backups require a private file destination, not stdout");
10304
- const path3 = resolve5(
10305
- opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
11317
+ const path3 = resolve6(
11318
+ opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID8().slice(0, 8)}${extension}`
10306
11319
  );
10307
11320
  if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
10308
- extname(path3).toLowerCase()
11321
+ extname2(path3).toLowerCase()
10309
11322
  ))
10310
11323
  throw new CliUsageError("Backup filename extension must match --file-format");
10311
- const { api, tsgId } = await context();
11324
+ const { api, tsgId } = await context2();
10312
11325
  const data = await backupRuntimeProfiles(api, tsgId, { profile, maxPages: opts.maxPages });
10313
- const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
11326
+ const content = opts.fileFormat === "yaml" ? dump7(data, { schema: JSON_SCHEMA2, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
10314
11327
  `;
10315
- if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
11328
+ if (Buffer.byteLength(content) > MAX_BACKUP_BYTES2)
10316
11329
  throw new Error("Backup exceeds the 20 MiB size limit; select individual profiles");
10317
11330
  await writeReportFile(path3, content);
10318
11331
  console.log(
@@ -10335,7 +11348,7 @@ function registerProfileTransferCommands(profiles2) {
10335
11348
  )
10336
11349
  );
10337
11350
  } catch (error) {
10338
- failure(error);
11351
+ failure2(error);
10339
11352
  }
10340
11353
  });
10341
11354
  const restore = profiles2.command("restore <file>").description(
@@ -10356,12 +11369,12 @@ function registerProfileTransferCommands(profiles2) {
10356
11369
  ).option(
10357
11370
  "--max-pages <n>",
10358
11371
  "Maximum pages per inventory; incomplete inventories fail",
10359
- maxPages,
11372
+ maxPages2,
10360
11373
  100
10361
11374
  ).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
10362
11375
  "--force",
10363
11376
  "Execute without a prompt; requires --expect-tsg and does not override conflicts"
10364
- ).option("--output <format>", FORMATS).addHelpText(
11377
+ ).option("--output <format>", FORMATS2).addHelpText(
10365
11378
  "after",
10366
11379
  examples(
10367
11380
  "airs runtime profiles restore ./profiles.json --dry-run --output json",
@@ -10377,24 +11390,24 @@ function registerProfileTransferCommands(profiles2) {
10377
11390
  if (opts.force && !opts.expectTsg)
10378
11391
  throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
10379
11392
  const mappings = dlpMap(opts.dlpMap);
10380
- const path3 = resolve5(file);
10381
- const extension = extname(path3).toLowerCase();
11393
+ const path3 = resolve6(file);
11394
+ const extension = extname2(path3).toLowerCase();
10382
11395
  if (![".json", ".yaml", ".yml"].includes(extension))
10383
11396
  throw new CliUsageError("Backup file must use .json, .yaml or .yml");
10384
- const info = await stat(path3);
10385
- if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
11397
+ const info = await stat2(path3);
11398
+ if (!info.isFile() || info.size > MAX_BACKUP_BYTES2)
10386
11399
  throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
10387
11400
  let input2;
10388
11401
  try {
10389
- const text2 = await readFile9(path3, "utf8");
10390
- if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
10391
- input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
11402
+ const text2 = await readFile10(path3, "utf8");
11403
+ if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES2) throw new Error("Backup too large");
11404
+ input2 = extension === ".json" ? JSON.parse(text2) : load3(text2, { schema: JSON_SCHEMA2 });
10392
11405
  } catch {
10393
11406
  throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
10394
11407
  }
10395
11408
  const data = RuntimeProfilesBackupSchema.parse(input2);
10396
11409
  const format = await resolveOutput(restore, opts);
10397
- const { api, tsgId } = await context();
11410
+ const { api, tsgId } = await context2();
10398
11411
  if (opts.expectTsg && opts.expectTsg !== tsgId)
10399
11412
  throw new CliUsageError(
10400
11413
  "--expect-tsg does not match the selected destination tenant; no API request was made"
@@ -10438,7 +11451,7 @@ function registerProfileTransferCommands(profiles2) {
10438
11451
  `DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
10439
11452
  );
10440
11453
  if (opts.dryRun) {
10441
- console.log(renderRestore(summary, columns, format));
11454
+ console.log(renderRestore2(summary, columns, format));
10442
11455
  return;
10443
11456
  }
10444
11457
  const changing = plan.profiles.filter(
@@ -10451,13 +11464,13 @@ function registerProfileTransferCommands(profiles2) {
10451
11464
  { action: `restore profiles into TSG ${tsgId}` }
10452
11465
  );
10453
11466
  const result = await restoreRuntimeProfiles(api, plan);
10454
- console.log(renderRestore({ ...result }, columns, format));
11467
+ console.log(renderRestore2({ ...result }, columns, format));
10455
11468
  if (!result.complete) {
10456
11469
  ui.error(result.error ?? "Restore incomplete");
10457
11470
  process.exitCode = 1;
10458
11471
  }
10459
11472
  } catch (error) {
10460
- failure(error);
11473
+ failure2(error);
10461
11474
  }
10462
11475
  });
10463
11476
  }
@@ -10565,8 +11578,8 @@ function registerCleanupCommand(parent) {
10565
11578
 
10566
11579
  // src/cli/commands/runtime-dashboard.ts
10567
11580
  import { lstat as lstat4 } from "fs/promises";
10568
- import { resolve as resolve6 } from "path";
10569
- import { dump as dump7 } from "js-yaml";
11581
+ import { resolve as resolve7 } from "path";
11582
+ import { dump as dump8 } from "js-yaml";
10570
11583
  var TIME_UNITS = ["hour", "hours", "day", "days"];
10571
11584
  function integer3(value, name, minimum = 0) {
10572
11585
  const n = Number(value);
@@ -10628,7 +11641,7 @@ function scanOptions(command) {
10628
11641
  function registerRuntimeDashboardCommands(runtime) {
10629
11642
  const dashboard = runtime.command("dashboard").description("SCM application summaries and daily telemetry (OAuth)");
10630
11643
  const sessions = runtime.command("sessions").description("Verified SCM session inventory and drill-down (OAuth)");
10631
- function register6(command, prepare) {
11644
+ function register7(command, prepare) {
10632
11645
  command.option("--output <format>", "Output format: pretty, json, yaml").action(async (opts) => {
10633
11646
  try {
10634
11647
  const read = prepare(opts);
@@ -10641,7 +11654,7 @@ function registerRuntimeDashboardCommands(runtime) {
10641
11654
  "Scan content requires exactly one of --show-content or --output-file <new-path>"
10642
11655
  );
10643
11656
  if (opts.outputFile) {
10644
- const path3 = resolve6(text(opts.outputFile, "--output-file"));
11657
+ const path3 = resolve7(text(opts.outputFile, "--output-file"));
10645
11658
  const existing = await lstat4(path3).catch((error) => {
10646
11659
  if (error.code !== "ENOENT") throw error;
10647
11660
  return void 0;
@@ -10655,9 +11668,9 @@ function registerRuntimeDashboardCommands(runtime) {
10655
11668
  numRetries: 0
10656
11669
  });
10657
11670
  const result = await read(client.dashboard);
10658
- const output = format === "yaml" ? dump7(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
11671
+ const output = format === "yaml" ? dump8(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
10659
11672
  if (command.name() === "scan-content" && opts.outputFile) {
10660
- await writeReportFile(resolve6(String(opts.outputFile)), `${output}
11673
+ await writeReportFile(resolve7(String(opts.outputFile)), `${output}
10661
11674
  `);
10662
11675
  ui.status("Scan content exported to the requested private file (0600).");
10663
11676
  } else console.log(output);
@@ -10671,7 +11684,7 @@ function registerRuntimeDashboardCommands(runtime) {
10671
11684
  }
10672
11685
  });
10673
11686
  }
10674
- register6(
11687
+ register7(
10675
11688
  pageOptions2(
10676
11689
  timeOptions(
10677
11690
  dashboard.command("applications").description("One page of application activity"),
@@ -10700,7 +11713,7 @@ function registerRuntimeDashboardCommands(runtime) {
10700
11713
  ["application", "application"],
10701
11714
  ["application-violations", "applicationViolationBreakdown"]
10702
11715
  ]) {
10703
- register6(
11716
+ register7(
10704
11717
  timeOptions(
10705
11718
  dashboard.command(name).requiredOption("--app-id <id>", "Application identity").requiredOption("--app-name <name>", "Exact dashboard application name"),
10706
11719
  "30",
@@ -10726,7 +11739,7 @@ function registerRuntimeDashboardCommands(runtime) {
10726
11739
  ["violations-trend", "applicationsViolationsTrend", false],
10727
11740
  ["apps-list", "appsList", true]
10728
11741
  ]) {
10729
- register6(
11742
+ register7(
10730
11743
  timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
10731
11744
  (opts) => {
10732
11745
  const query2 = window(opts);
@@ -10734,11 +11747,11 @@ function registerRuntimeDashboardCommands(runtime) {
10734
11747
  }
10735
11748
  );
10736
11749
  }
10737
- register6(timeOptions(sessions.command("chart")), (opts) => {
11750
+ register7(timeOptions(sessions.command("chart")), (opts) => {
10738
11751
  const query2 = window(opts);
10739
11752
  return (client) => client.sessionsChart(query2);
10740
11753
  });
10741
- register6(
11754
+ register7(
10742
11755
  pageOptions2(
10743
11756
  timeOptions(
10744
11757
  sessions.command("list").description("Session entries; --all walks pages with completeness checks")
@@ -10804,7 +11817,7 @@ function registerRuntimeDashboardCommands(runtime) {
10804
11817
  };
10805
11818
  }
10806
11819
  );
10807
- register6(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
11820
+ register7(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
10808
11821
  const query2 = {
10809
11822
  ...identity(opts),
10810
11823
  limit: integer3(opts.limit, "--limit", 1),
@@ -10812,11 +11825,11 @@ function registerRuntimeDashboardCommands(runtime) {
10812
11825
  };
10813
11826
  return (client) => client.session(query2);
10814
11827
  });
10815
- register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
11828
+ register7(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
10816
11829
  const query2 = { ...identity(opts), ...scan(opts) };
10817
11830
  return (client) => client.sessionTransaction(query2);
10818
11831
  });
10819
- register6(
11832
+ register7(
10820
11833
  scanOptions(
10821
11834
  sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
10822
11835
  ).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
@@ -10829,8 +11842,8 @@ function registerRuntimeDashboardCommands(runtime) {
10829
11842
 
10830
11843
  // src/cli/commands/runtime-report.ts
10831
11844
  import { lstat as lstat5 } from "fs/promises";
10832
- import { resolve as resolve7 } from "path";
10833
- import { randomUUID as randomUUID8 } from "crypto";
11845
+ import { resolve as resolve8 } from "path";
11846
+ import { randomUUID as randomUUID9 } from "crypto";
10834
11847
  function registerRuntimeReportCommand(runtime) {
10835
11848
  const command = runtime.command("report").description("Generate a daily AI Runtime Security environment report (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
10836
11849
  "--output-file <path>",
@@ -10847,15 +11860,15 @@ function registerRuntimeReportCommand(runtime) {
10847
11860
  const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
10848
11861
  if (format !== "html" && format !== "markdown")
10849
11862
  usageError("Report output must be html or markdown");
10850
- const maxPages2 = Number(opts.maxPages);
10851
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
11863
+ const maxPages3 = Number(opts.maxPages);
11864
+ if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
10852
11865
  usageError("--max-pages must be an integer from 1 to 100");
10853
11866
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
10854
11867
  usageError("--title must contain 1\u2013240 characters");
10855
11868
  if (opts.outputFile !== void 0 && !opts.outputFile.trim())
10856
11869
  usageError("--output-file cannot be empty");
10857
- const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID8().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
10858
- const outputPath = opts.outputFile === "-" ? void 0 : resolve7(opts.outputFile ?? defaultName);
11870
+ const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID9().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
11871
+ const outputPath = opts.outputFile === "-" ? void 0 : resolve8(opts.outputFile ?? defaultName);
10859
11872
  try {
10860
11873
  if (outputPath) {
10861
11874
  const existing = await lstat5(outputPath).catch((error) => {
@@ -10870,7 +11883,7 @@ function registerRuntimeReportCommand(runtime) {
10870
11883
  numRetries: 0
10871
11884
  });
10872
11885
  ui.status("Collecting daily runtime activity and current configuration (read-only)...");
10873
- const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages2 });
11886
+ const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages3 });
10874
11887
  const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
10875
11888
  if (outputPath) {
10876
11889
  await writeReportFile(outputPath, rendered);
@@ -11030,7 +12043,7 @@ function registerCreateCommand(parent) {
11030
12043
  }
11031
12044
 
11032
12045
  // src/cli/commands/topics-eval.ts
11033
- import { readFile as readFile10 } from "fs/promises";
12046
+ import { readFile as readFile11 } from "fs/promises";
11034
12047
 
11035
12048
  // src/core/prompt-loader.ts
11036
12049
  function parseCsvLine(line) {
@@ -11173,7 +12186,7 @@ function registerEvalCommand(parent) {
11173
12186
  resolveDeprecatedAliases(cmd, opts);
11174
12187
  try {
11175
12188
  const config = await loadConfig();
11176
- const csvContent = await readFile10(opts.prompts, "utf-8");
12189
+ const csvContent = await readFile11(opts.prompts, "utf-8");
11177
12190
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
11178
12191
  if (!config.airsApiKey && !config.airsApiToken) {
11179
12192
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
@@ -11345,7 +12358,7 @@ function completedBulkResults(state) {
11345
12358
  }
11346
12359
  async function writeBulkResults(outputPath, results) {
11347
12360
  await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
11348
- const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID9()}`;
12361
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
11349
12362
  try {
11350
12363
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
11351
12364
  encoding: "utf-8",
@@ -11472,7 +12485,7 @@ function registerRuntimeCommand(program) {
11472
12485
  if (!config.airsApiKey && !config.airsApiToken) {
11473
12486
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
11474
12487
  }
11475
- const raw = await readFile11(opts.file, "utf-8");
12488
+ const raw = await readFile12(opts.file, "utf-8");
11476
12489
  const prompts = parseInputFile(raw, opts.file);
11477
12490
  if (prompts.length === 0) {
11478
12491
  usageError("No prompts found in input file");
@@ -12130,7 +13143,7 @@ function registerRuntimeCommand(program) {
12130
13143
  }
12131
13144
 
12132
13145
  // src/config/tenant-settings.ts
12133
- import { randomUUID as randomUUID10 } from "crypto";
13146
+ import { randomUUID as randomUUID11 } from "crypto";
12134
13147
  import { constants } from "fs";
12135
13148
  import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
12136
13149
  import { dirname as dirname4, join as join4 } from "path";
@@ -12160,7 +13173,7 @@ async function createManagedTenant(name, config) {
12160
13173
  throw new Error("Tenant name already exists");
12161
13174
  const directory = join4(dirname4(tenantStorePath()), "configs");
12162
13175
  await mkdir3(directory, { recursive: true, mode: 448 });
12163
- const path3 = join4(directory, `${name}-${randomUUID10()}.json`);
13176
+ const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
12164
13177
  const file = await open2(path3, "wx", 384);
12165
13178
  try {
12166
13179
  const data = Object.fromEntries(
@@ -12190,11 +13203,11 @@ async function setTenantSetting(name, key, value) {
12190
13203
  "Cannot lock tenant config; it may be read-only or another edit is in progress"
12191
13204
  );
12192
13205
  });
12193
- const temporary = join4(dirname4(path3), `.airs-config-${randomUUID10()}.tmp`);
13206
+ const temporary = join4(dirname4(path3), `.airs-config-${randomUUID11()}.tmp`);
12194
13207
  let created = false;
12195
13208
  try {
12196
- const stat2 = await lstat6(path3);
12197
- if (!stat2.isFile() || (stat2.mode & 146) === 0)
13209
+ const stat3 = await lstat6(path3);
13210
+ if (!stat3.isFile() || (stat3.mode & 146) === 0)
12198
13211
  throw new Error("Tenant config is not a writable regular file");
12199
13212
  await access(path3, constants.W_OK);
12200
13213
  const current = readTenantConfigFile(path3, entry.tsgId);
@@ -12490,7 +13503,7 @@ function buildProgram() {
12490
13503
  if (root.debug) {
12491
13504
  const logPath = join5(
12492
13505
  process.cwd(),
12493
- `debug-api-${Date.now()}-${randomUUID11().slice(0, 8)}.jsonl`
13506
+ `debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
12494
13507
  );
12495
13508
  try {
12496
13509
  let ancestor = actionCommand;