@cdot65/prisma-airs-cli 5.9.0 → 5.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +416 -74
  2. package/package.json +2 -2
package/dist/cli/index.js CHANGED
@@ -51,7 +51,7 @@ function handleUnhandledRejection(reason) {
51
51
  const message = reason instanceof Error ? reason.message : String(reason);
52
52
  console.error(chalk.red(`
53
53
  Unexpected error: ${message}`));
54
- console.error(chalk.dim(" Re-run with --debug to capture full API traffic.\n"));
54
+ console.error(chalk.dim(" Re-run with --debug to capture redacted API diagnostics.\n"));
55
55
  process.exit(1);
56
56
  }
57
57
  function installProcessGuards() {
@@ -358,7 +358,7 @@ function fail(err) {
358
358
  \u2717 Error: ${message}`));
359
359
  if (status !== void 0) {
360
360
  console.error(chalk2.red(` HTTP ${status}`));
361
- console.error(chalk2.dim(" Re-run with --debug to capture full API traffic."));
361
+ console.error(chalk2.dim(" Re-run with --debug to capture redacted API diagnostics."));
362
362
  }
363
363
  console.error("");
364
364
  process.exit(1);
@@ -8768,6 +8768,7 @@ var topicsView = {
8768
8768
  // src/cli/commands/dlp/dictionaries.ts
8769
8769
  import { readFile as readFile8 } from "fs/promises";
8770
8770
  import { basename as basename2 } from "path";
8771
+ import { AISecSDKException as AISecSDKException2, ErrorType as ErrorType3 } from "@cdot65/prisma-airs-sdk";
8771
8772
 
8772
8773
  // src/airs/dlp/dictionaries.ts
8773
8774
  var SdkDictionariesService = class {
@@ -8882,12 +8883,40 @@ function visibleRecords(records, includePredefined) {
8882
8883
  }
8883
8884
 
8884
8885
  // src/cli/commands/dlp/dictionaries.ts
8886
+ async function readMetadata(path3) {
8887
+ const raw = await readFile8(path3, "utf-8");
8888
+ let value;
8889
+ try {
8890
+ value = JSON.parse(raw);
8891
+ } catch {
8892
+ throw new CliUsageError("Invalid JSON in dictionary metadata file");
8893
+ }
8894
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8895
+ throw new CliUsageError("Dictionary metadata must be a JSON object");
8896
+ }
8897
+ return value;
8898
+ }
8899
+ function dictionaryFailure(err) {
8900
+ if (err instanceof AISecSDKException2 && err.errorType === ErrorType3.USER_REQUEST_PAYLOAD_ERROR) {
8901
+ fail(new CliUsageError("Invalid dictionary metadata: verify required fields and value types"));
8902
+ }
8903
+ fail(err);
8904
+ }
8905
+ function uploadFailure(err) {
8906
+ const error = err;
8907
+ if ((error?.status ?? error?.statusCode) === 400 && !error?.problem?.detail && !error?.problem?.errors?.length) {
8908
+ console.error(
8909
+ 'Dictionary upload rejected. Verify the SCM region display name (for example, --region "United States"); region codes such as GLOBAL or us-west-2 are not equivalent.'
8910
+ );
8911
+ }
8912
+ dictionaryFailure(err);
8913
+ }
8885
8914
  async function buildMetadata(opts) {
8886
8915
  if (opts.metadataFile) {
8887
- return JSON.parse(await readFile8(opts.metadataFile, "utf-8"));
8916
+ return await readMetadata(opts.metadataFile);
8888
8917
  }
8889
8918
  if (!opts.name || !opts.category || !opts.region || !opts.file) {
8890
- throw new Error("--name, --category, --region, and --file are required");
8919
+ throw new CliUsageError("--name, --category, --region, and --file are required");
8891
8920
  }
8892
8921
  return {
8893
8922
  name: opts.name,
@@ -8914,7 +8943,7 @@ function register(dlp) {
8914
8943
  const svc = new SdkDictionariesService(await loadDlpClientOptions());
8915
8944
  const params = {
8916
8945
  size,
8917
- sort: opts.sort,
8946
+ sort: opts.sort ?? ["name,asc"],
8918
8947
  keywords: includeKeywords ? true : void 0
8919
8948
  };
8920
8949
  const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : (await svc.list({ ...params, page })).content;
@@ -8927,11 +8956,11 @@ function register(dlp) {
8927
8956
  fail(err);
8928
8957
  }
8929
8958
  });
8930
- 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) => {
8959
+ group.command("create").description("Create dictionary via multipart upload").option("--name <s>", "").option("--category <s>", "").option("--region <s>", 'SCM region display name, e.g. "United States"').option("--description <s>", "").option("--classification <s>", "Legacy top-level metadata field (server support unverified)").option("--file <path>", "Keyword file: TXT (one keyword per line), or CSV (header required)").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) => {
8931
8960
  try {
8932
8961
  const format = await resolveOutput(command, opts);
8933
8962
  const metadata = await buildMetadata(opts);
8934
- if (!opts.file) throw new Error("--file is required (multipart upload)");
8963
+ if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
8935
8964
  const file = await readFile8(opts.file);
8936
8965
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
8937
8966
  metadata,
@@ -8940,7 +8969,7 @@ function register(dlp) {
8940
8969
  });
8941
8970
  dlpDictionaries.renderCreated(r, format);
8942
8971
  } catch (err) {
8943
- usageError(err instanceof Error ? err.message : String(err));
8972
+ uploadFailure(err);
8944
8973
  }
8945
8974
  });
8946
8975
  const getCmd = group.command("get <id>").option("--keywords", "").option("--include-keywords", "Alias for --keywords").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (id, opts) => {
@@ -8958,11 +8987,11 @@ function register(dlp) {
8958
8987
  });
8959
8988
  group.command("replace <id>").description(
8960
8989
  'Full-replace via multipart upload. --file required. May return 200 (body) or 204 (re-get; falls back to "(state not echoed)" on transient failure).'
8961
- ).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) => {
8990
+ ).option("--name <s>", "").option("--category <s>", "").option("--region <s>", 'SCM region display name, e.g. "United States"').option("--description <s>", "").option("--classification <s>", "Legacy top-level metadata field (server support unverified)").option("--file <path>", "Required: TXT (one keyword per line), or CSV (header required)").option("--metadata-file <path>", "JSON metadata file").option("--include-keywords", "").option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
8962
8991
  try {
8963
8992
  const metadata = await buildMetadata(opts);
8964
8993
  const format = await resolveOutput(command, opts);
8965
- if (!opts.file) throw new Error("--file is required (multipart upload)");
8994
+ if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
8966
8995
  const file = await readFile8(opts.file);
8967
8996
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
8968
8997
  metadata,
@@ -8975,23 +9004,34 @@ function register(dlp) {
8975
9004
  dlpDictionaries.renderReplaced(r, format);
8976
9005
  }
8977
9006
  } catch (err) {
8978
- usageError(err instanceof Error ? err.message : String(err));
9007
+ uploadFailure(err);
8979
9008
  }
8980
9009
  });
8981
9010
  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) => {
8982
9011
  try {
8983
9012
  const format = await resolveOutput(command, opts);
8984
9013
  if (opts.bodyFile && (opts.set || opts.clear)) {
8985
- throw new Error("--body-file is mutually exclusive with --set/--clear");
9014
+ throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
9015
+ }
9016
+ let body;
9017
+ if (opts.bodyFile) {
9018
+ body = await readMetadata(opts.bodyFile);
9019
+ } else {
9020
+ try {
9021
+ body = buildMergePatch({ set: opts.set, clear: opts.clear });
9022
+ } catch {
9023
+ throw new CliUsageError(
9024
+ "Invalid dictionary patch flags: use --set key=value, --clear key, or --body-file for nested fields"
9025
+ );
9026
+ }
8986
9027
  }
8987
- const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
8988
9028
  dlpDictionaries.renderPatched(
8989
9029
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
8990
9030
  await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
8991
9031
  format
8992
9032
  );
8993
9033
  } catch (err) {
8994
- usageError(err instanceof Error ? err.message : String(err));
9034
+ dictionaryFailure(err);
8995
9035
  }
8996
9036
  });
8997
9037
  group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
@@ -9108,31 +9148,70 @@ function buildFilteringProfileBody(opts) {
9108
9148
  if (opts.fileType?.length) body.file_type = opts.fileType;
9109
9149
  return body;
9110
9150
  }
9111
- function buildProfileBody(opts) {
9151
+ function validateProfileType(value) {
9152
+ if (value === "basic") {
9153
+ throw new Error(
9154
+ "Basic profile writes are unsupported: the API returned advanced for basic requests. Use advanced or manage basic profiles in SCM."
9155
+ );
9156
+ }
9157
+ if (value !== void 0 && value !== "advanced") {
9158
+ throw new Error("Profile type must be advanced");
9159
+ }
9160
+ }
9161
+ function validateProfileName(name) {
9162
+ if (typeof name !== "string" || !name.trim()) {
9163
+ throw new Error("Profile name is required and must be a nonblank string");
9164
+ }
9165
+ if (name.length > 32) throw new Error("Profile name must be at most 32 characters");
9166
+ }
9167
+ function validateProfileFlags(opts) {
9112
9168
  if (!opts.name) throw new Error("--name is required");
9113
- const profileType = opts.profileType ?? "advanced";
9169
+ validateProfileName(opts.name);
9170
+ validateProfileType(opts.profileType);
9171
+ if (!["and", "or", "not", "and_not", "or_not"].includes((opts.combinator ?? "or").toLowerCase())) {
9172
+ throw new Error("--combinator must be one of and|or|not|and_not|or_not");
9173
+ }
9174
+ if (!["low", "medium", "high"].includes(opts.confidence ?? "high")) {
9175
+ throw new Error("--confidence must be low|medium|high");
9176
+ }
9177
+ }
9178
+ function buildProfileBody(opts, patterns = []) {
9179
+ validateProfileFlags(opts);
9114
9180
  const body = {
9115
9181
  name: opts.name,
9116
- profile_type: profileType
9182
+ profile_type: "advanced"
9117
9183
  };
9118
9184
  if (opts.description != null) body.description = opts.description;
9119
9185
  if (opts.granular != null) body.is_granular_data_profile = !!opts.granular;
9120
9186
  if (opts.patternId?.length) {
9121
- const op = (opts.combinator ?? "or").toLowerCase();
9122
- if (!["and", "or", "not", "and_not", "or_not"].includes(op)) {
9123
- throw new Error(`--combinator must be one of and|or|not|and_not|or_not (got '${op}')`);
9124
- }
9187
+ const confidence = opts.confidence ?? "high";
9188
+ const leaves = opts.patternId.map((id) => {
9189
+ const pattern = patterns.find((item) => item.id === id);
9190
+ if (!pattern)
9191
+ throw new Error("Every --pattern-id must resolve before building profile rules");
9192
+ if (pattern.confidenceLevels?.length && !pattern.confidenceLevels.includes(confidence)) {
9193
+ throw new Error("Requested confidence is not supported by every referenced pattern");
9194
+ }
9195
+ return {
9196
+ rule_item: {
9197
+ id: pattern.id,
9198
+ name: pattern.name,
9199
+ version: pattern.version,
9200
+ detection_technique: pattern.technique,
9201
+ confidence_level: confidence,
9202
+ supported_confidence_levels: pattern.confidenceLevels?.length ? [...pattern.confidenceLevels] : [confidence],
9203
+ match_type: "include",
9204
+ occurrence_operator_type: "more_than_equal_to",
9205
+ occurrence_count: 1
9206
+ }
9207
+ };
9208
+ });
9125
9209
  body.detection_rules = [
9126
9210
  {
9127
9211
  rule_type: "expression_tree",
9128
9212
  expression_tree: {
9129
- operator_type: op,
9130
- condition_pattern: opts.patternId.map((id) => ({
9131
- data_pattern_id: id,
9132
- confidence_level: opts.confidence ?? "high",
9133
- occurrence_operator_type: "any",
9134
- occurrence_count: 1
9135
- }))
9213
+ operator_type: (opts.combinator ?? "or").toLowerCase(),
9214
+ sub_expressions: leaves
9136
9215
  }
9137
9216
  }
9138
9217
  ];
@@ -9170,8 +9249,8 @@ function register2(dlp) {
9170
9249
  try {
9171
9250
  const { page, size } = resolvePageParams(listCmd, opts);
9172
9251
  const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9173
- const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9174
- const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort });
9252
+ const all = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : void 0;
9253
+ const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] });
9175
9254
  dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
9176
9255
  } catch (err) {
9177
9256
  fail(err);
@@ -9341,7 +9420,7 @@ function register4(dlp) {
9341
9420
  try {
9342
9421
  const { page, size } = resolvePageParams(listCmd, opts);
9343
9422
  const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9344
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
9423
+ const result = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] })).content;
9345
9424
  const visible = visibleRecords(result, opts.includePredefined);
9346
9425
  dlpPatterns.renderList(
9347
9426
  { content: visible, totalElements: visible.length },
@@ -9419,6 +9498,9 @@ function register4(dlp) {
9419
9498
  });
9420
9499
  }
9421
9500
 
9501
+ // src/cli/commands/dlp/profiles.ts
9502
+ import { AISecSDKException as AISecSDKException3, ErrorType as ErrorType4 } from "@cdot65/prisma-airs-sdk";
9503
+
9422
9504
  // src/airs/dlp/data-profiles.ts
9423
9505
  var SdkDataProfilesService = class {
9424
9506
  client;
@@ -9456,7 +9538,7 @@ function listFlags3(cmd) {
9456
9538
  return cmd;
9457
9539
  }
9458
9540
  function writeFlags2(cmd) {
9459
- return cmd.option("--name <s>", "Profile name (required unless --body-file)").option("--profile-type <s>", "Profile type: basic|advanced (default: advanced)").option("--description <s>", "Description").option("--granular", "Granular data profile").option(
9541
+ return cmd.option("--name <s>", "Profile name (required unless --body-file)").option("--profile-type <s>", "Profile type: advanced (basic writes are unsupported)").option("--description <s>", "Description").option("--granular", "Granular data profile").option(
9460
9542
  "--pattern-id <id>",
9461
9543
  "Data pattern ID to include (repeatable). Builds a simple expression_tree.",
9462
9544
  repeatable
@@ -9468,16 +9550,75 @@ function writeFlags2(cmd) {
9468
9550
  "Raw JSON body file (escape hatch; required for complex rule trees)"
9469
9551
  ).option("--output <fmt>", "Output format", "pretty");
9470
9552
  }
9553
+ function validateBodyType(body) {
9554
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
9555
+ throw new CliUsageError("Profile body must be a JSON object");
9556
+ }
9557
+ try {
9558
+ validateProfileName(body.name);
9559
+ validateProfileType(body.profile_type);
9560
+ } catch (error) {
9561
+ throw new CliUsageError(error.message);
9562
+ }
9563
+ }
9564
+ function profileFailure(error) {
9565
+ if (error instanceof AISecSDKException3 && error.errorType === ErrorType4.USER_REQUEST_PAYLOAD_ERROR) {
9566
+ fail(new CliUsageError("Invalid profile metadata: verify required fields and value types"));
9567
+ }
9568
+ fail(error);
9569
+ }
9471
9570
  async function resolveWriteBody2(opts) {
9472
- if (opts.body || opts.bodyFile) {
9473
- const body = await parseBody({
9474
- body: opts.body,
9475
- bodyFile: opts.bodyFile
9476
- });
9477
- if (!body) throw new Error("--body or --body-file was empty");
9571
+ try {
9572
+ validateProfileType(opts.profileType);
9573
+ } catch (error) {
9574
+ throw new CliUsageError(error.message);
9575
+ }
9576
+ if (opts.body !== void 0 || opts.bodyFile !== void 0) {
9577
+ if (opts.body !== void 0 && opts.bodyFile !== void 0) {
9578
+ throw new CliUsageError("--body and --body-file are mutually exclusive");
9579
+ }
9580
+ let body;
9581
+ try {
9582
+ body = await parseBody({ body: opts.body, bodyFile: opts.bodyFile });
9583
+ } catch {
9584
+ throw new CliUsageError("Cannot read profile body as JSON; verify the file and JSON syntax");
9585
+ }
9586
+ validateBodyType(body);
9478
9587
  return body;
9479
9588
  }
9480
- return buildProfileBody(opts);
9589
+ try {
9590
+ validateProfileFlags(opts);
9591
+ } catch (error) {
9592
+ throw new CliUsageError(error.message);
9593
+ }
9594
+ const patterns = [];
9595
+ const ids = opts.patternId;
9596
+ if (ids?.length) {
9597
+ const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9598
+ for (const id of new Set(ids)) {
9599
+ const pattern = await svc.get(id);
9600
+ if (pattern.id !== id || !pattern.name || !pattern.detection_config?.technique || !Number.isInteger(pattern.version) || Number(pattern.version) < 1) {
9601
+ throw new Error(
9602
+ "Referenced pattern did not return a matching ID, name, technique, and version; no profile was written"
9603
+ );
9604
+ }
9605
+ if (pattern.status !== "active") {
9606
+ throw new Error("Referenced pattern is not active; no profile was written");
9607
+ }
9608
+ patterns.push({
9609
+ id: pattern.id,
9610
+ name: pattern.name,
9611
+ version: pattern.version,
9612
+ technique: pattern.detection_config.technique,
9613
+ confidenceLevels: pattern.detection_config.supported_confidence_levels
9614
+ });
9615
+ }
9616
+ }
9617
+ try {
9618
+ return buildProfileBody(opts, patterns);
9619
+ } catch (error) {
9620
+ throw new CliUsageError(error.message);
9621
+ }
9481
9622
  }
9482
9623
  function register5(dlp) {
9483
9624
  const group = dlp.command("profiles").description(
@@ -9490,7 +9631,7 @@ function register5(dlp) {
9490
9631
  try {
9491
9632
  const { page, size } = resolvePageParams(listCmd, opts);
9492
9633
  const svc = new SdkDataProfilesService(await loadDlpClientOptions());
9493
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
9634
+ const result = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] })).content;
9494
9635
  const visible = visibleRecords(result, opts.includePredefined);
9495
9636
  dlpProfiles.renderList(
9496
9637
  { content: visible, totalElements: visible.length },
@@ -9511,7 +9652,7 @@ function register5(dlp) {
9511
9652
  format
9512
9653
  );
9513
9654
  } catch (err) {
9514
- usageError(err instanceof Error ? err.message : String(err));
9655
+ profileFailure(err);
9515
9656
  }
9516
9657
  }
9517
9658
  );
@@ -9536,7 +9677,7 @@ function register5(dlp) {
9536
9677
  format
9537
9678
  );
9538
9679
  } catch (err) {
9539
- usageError(err instanceof Error ? err.message : String(err));
9680
+ profileFailure(err);
9540
9681
  }
9541
9682
  }
9542
9683
  );
@@ -9546,16 +9687,24 @@ function register5(dlp) {
9546
9687
  try {
9547
9688
  const format = await resolveOutput(command, opts);
9548
9689
  if (opts.bodyFile && (opts.set || opts.clear)) {
9549
- throw new Error("--body-file is mutually exclusive with --set/--clear");
9690
+ throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
9550
9691
  }
9551
- const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9692
+ let body;
9693
+ try {
9694
+ body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
9695
+ } catch {
9696
+ throw new CliUsageError(
9697
+ "Invalid profile patch: verify JSON syntax and --set key=value/--clear key flags"
9698
+ );
9699
+ }
9700
+ validateBodyType(body);
9552
9701
  dlpProfiles.renderPatched(
9553
9702
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9554
9703
  await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
9555
9704
  format
9556
9705
  );
9557
9706
  } catch (err) {
9558
- usageError(err instanceof Error ? err.message : String(err));
9707
+ profileFailure(err);
9559
9708
  }
9560
9709
  });
9561
9710
  group.command("delete <id>").description("Not supported \u2014 explains the cleanup limitation and exits 2").action((id) => {
@@ -9573,7 +9722,7 @@ function register5(dlp) {
9573
9722
  import { randomUUID as randomUUID7 } from "crypto";
9574
9723
  import { readFile as readFile9, stat } from "fs/promises";
9575
9724
  import { extname, resolve as resolve5 } from "path";
9576
- import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot65/prisma-airs-sdk";
9725
+ import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
9577
9726
  import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
9578
9727
  import { ZodError } from "zod";
9579
9728
 
@@ -9632,13 +9781,79 @@ function assertName(name) {
9632
9781
  async function springPages(fetch, maxPages3, label) {
9633
9782
  pagesLimit(maxPages3);
9634
9783
  const items = [];
9784
+ const ids = /* @__PURE__ */ new Set();
9785
+ let totalPages;
9786
+ let totalElements;
9787
+ let effectiveSize;
9788
+ const inconsistent = () => {
9789
+ throw new Error(
9790
+ `Inventory of ${label} has inconsistent pagination metadata; retry the operation`
9791
+ );
9792
+ };
9793
+ const alias = (camel, snake) => {
9794
+ if (camel !== void 0 && snake !== void 0 && camel !== snake) inconsistent();
9795
+ return camel ?? snake;
9796
+ };
9635
9797
  for (let page = 0; page < maxPages3; page++) {
9636
9798
  const result = await fetch(page);
9637
- items.push(...result.content);
9638
- if (items.length > 1e4)
9799
+ const pageSize = alias(result.pageable?.pageSize, result.pageable?.page_size);
9800
+ const suppliedSize = alias(result.size, pageSize);
9801
+ if (suppliedSize !== void 0 && (!Number.isSafeInteger(suppliedSize) || suppliedSize < 1 || suppliedSize > 100 || effectiveSize !== void 0 && suppliedSize !== effectiveSize))
9802
+ inconsistent();
9803
+ effectiveSize = suppliedSize ?? effectiveSize ?? 100;
9804
+ const suppliedPages = alias(result.totalPages, result.total_pages);
9805
+ const suppliedElements = alias(result.totalElements, result.total_elements);
9806
+ if (items.length + result.content.length > 1e4)
9639
9807
  throw new Error(`Inventory of ${label} exceeds the 10000-record safety limit`);
9640
- const last = result.last ?? (result.totalPages !== void 0 ? page + 1 >= result.totalPages : result.content.length < 100);
9641
- if (last) return items;
9808
+ for (const [actual, expected] of [
9809
+ [result.number, page],
9810
+ [result.size, effectiveSize],
9811
+ [alias(result.numberOfElements, result.number_of_elements), result.content.length],
9812
+ [result.first, page === 0],
9813
+ [result.empty, result.content.length === 0],
9814
+ [alias(result.pageable?.pageNumber, result.pageable?.page_number), page],
9815
+ [pageSize, effectiveSize],
9816
+ [result.pageable?.offset, page * effectiveSize]
9817
+ ]) {
9818
+ if (actual !== void 0 && actual !== expected) inconsistent();
9819
+ }
9820
+ for (const [actual, previous] of [
9821
+ [suppliedPages, totalPages],
9822
+ [suppliedElements, totalElements]
9823
+ ]) {
9824
+ if (actual !== void 0 && (!Number.isSafeInteger(actual) || actual < 0 || previous !== void 0 && actual !== previous))
9825
+ inconsistent();
9826
+ }
9827
+ totalPages = suppliedPages ?? totalPages;
9828
+ totalElements = suppliedElements ?? totalElements;
9829
+ if (totalPages !== void 0 && totalElements !== void 0 && totalPages !== Math.ceil(totalElements / effectiveSize))
9830
+ inconsistent();
9831
+ if (totalPages !== void 0 && (page >= Math.max(totalPages, 1) || totalPages === 0 && result.content.length !== 0))
9832
+ inconsistent();
9833
+ if (totalElements !== void 0 && result.content.length !== Math.min(effectiveSize, Math.max(0, totalElements - page * effectiveSize)))
9834
+ inconsistent();
9835
+ for (const item of result.content) {
9836
+ if (typeof item.id !== "string" || !item.id.trim())
9837
+ throw new Error(
9838
+ `Inventory of ${label} contains a missing resource ID; retry the operation`
9839
+ );
9840
+ if (ids.has(item.id))
9841
+ throw new Error(
9842
+ `Inventory of ${label} contains duplicate resource IDs; retry the operation`
9843
+ );
9844
+ ids.add(item.id);
9845
+ items.push(item);
9846
+ }
9847
+ const expectedLast = totalPages !== void 0 ? page + 1 >= totalPages : totalElements !== void 0 ? items.length === totalElements : void 0;
9848
+ if (result.last !== void 0 && expectedLast !== void 0 && result.last !== expectedLast)
9849
+ inconsistent();
9850
+ const last = result.last ?? expectedLast ?? result.content.length < effectiveSize;
9851
+ if (totalPages !== void 0 && totalPages > 0 && (!last && result.content.length !== effectiveSize || last && result.content.length === 0))
9852
+ inconsistent();
9853
+ if (last) {
9854
+ if (totalElements !== void 0 && items.length !== totalElements) inconsistent();
9855
+ return items;
9856
+ }
9642
9857
  if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
9643
9858
  }
9644
9859
  throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
@@ -9646,17 +9861,17 @@ async function springPages(fetch, maxPages3, label) {
9646
9861
  async function inventories(api, maxPages3) {
9647
9862
  return {
9648
9863
  dictionaries: await springPages(
9649
- (page) => api.dictionaries.list({ page, size: 100, keywords: true }),
9864
+ (page) => api.dictionaries.list({ page, size: 100, sort: ["name,asc"], keywords: true }),
9650
9865
  maxPages3,
9651
9866
  "dictionaries"
9652
9867
  ),
9653
9868
  patterns: await springPages(
9654
- (page) => api.patterns.list({ page, size: 100 }),
9869
+ (page) => api.patterns.list({ page, size: 100, sort: ["name,asc"] }),
9655
9870
  maxPages3,
9656
9871
  "data patterns"
9657
9872
  ),
9658
9873
  profiles: await springPages(
9659
- (page) => api.profiles.list({ page, size: 100 }),
9874
+ (page) => api.profiles.list({ page, size: 100, sort: ["name,asc"] }),
9660
9875
  maxPages3,
9661
9876
  "data profiles"
9662
9877
  )
@@ -9786,12 +10001,14 @@ async function backupDlpResources(api, tsgId, options = {}) {
9786
10001
  if (leaf.technique === "dictionary") {
9787
10002
  const record = dictionaryById.get(id);
9788
10003
  if (!record) throw new Error(`references an unknown dictionary: ${id}`);
9789
- if (leaf.value.version != null)
10004
+ if (leaf.value.version != null && leaf.value.version !== 1)
9790
10005
  throw new Error(`references a version-pinned dictionary: ${id}`);
9791
10006
  addDictionary(record);
9792
10007
  } else {
9793
10008
  const record = patternById.get(id);
9794
10009
  if (!record) throw new Error(`references an unknown data pattern: ${id}`);
10010
+ if (record.detection_config?.technique !== leaf.technique)
10011
+ throw new Error("references a data pattern with a different detection technique");
9795
10012
  if (RETIRED_PATTERN.has(record.status ?? "active"))
9796
10013
  throw new Error(`references a retired data pattern: ${record.name ?? id}`);
9797
10014
  if (leaf.value.version != null && record.version !== leaf.value.version)
@@ -9878,6 +10095,20 @@ function keywordSet(value) {
9878
10095
  return void 0;
9879
10096
  return [...value].sort();
9880
10097
  }
10098
+ function dictionaryFile(metadata, keywords) {
10099
+ if (!keywords.length || keywords.some((word) => !word.length || /[\r\n]/.test(word)))
10100
+ throw new Error("Dictionary contains keywords that cannot be faithfully uploaded");
10101
+ if (/\.csv$/i.test(metadata.original_file_name)) {
10102
+ if (keywords.some((word) => word.includes(",")))
10103
+ throw new Error("Dictionary CSV contains unsupported multi-column keywords");
10104
+ return `keywords
10105
+ ${keywords.join("\n")}
10106
+ `;
10107
+ }
10108
+ if (/\.txt$/i.test(metadata.original_file_name)) return `${keywords.join("\n")}
10109
+ `;
10110
+ throw new Error("Dictionary filename must end in .csv or .txt for a verified upload");
10111
+ }
9881
10112
  function dictionaryEcho(metadata, record, keywords) {
9882
10113
  const { original_file_name, ...projection } = metadata;
9883
10114
  const echo = compareEcho(projection, record);
@@ -9893,7 +10124,8 @@ function dictionaryRequest(source, name) {
9893
10124
  original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
9894
10125
  region_name: source.region_name,
9895
10126
  ...source.description != null ? { description: source.description } : {},
9896
- ...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {}
10127
+ ...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {},
10128
+ ...source.tags != null ? { tags: structuredClone(source.tags) } : {}
9897
10129
  };
9898
10130
  const parsed = DictionaryRequestSchema.safeParse(metadata);
9899
10131
  if (!parsed.success)
@@ -9951,6 +10183,24 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9951
10183
  uniqueNames(backup.dictionaries, "dictionary");
9952
10184
  uniqueNames(backup.patterns, "data pattern");
9953
10185
  uniqueNames(backup.profiles, "data profile");
10186
+ const sourceBindings = /* @__PURE__ */ new Map();
10187
+ for (const [kind, records] of [
10188
+ ["dictionary", backup.dictionaries],
10189
+ ["pattern", backup.patterns]
10190
+ ]) {
10191
+ for (const record of records) {
10192
+ if (!record.id) continue;
10193
+ if (sourceBindings.has(record.id))
10194
+ throw new Error("Backup contains duplicate dependency identities");
10195
+ sourceBindings.set(record.id, {
10196
+ id: record.id,
10197
+ name: record.name ?? "",
10198
+ kind,
10199
+ technique: kind === "dictionary" ? "dictionary" : record.detection_config?.technique,
10200
+ version: kind === "dictionary" ? 1 : record.version ?? void 0
10201
+ });
10202
+ }
10203
+ }
9954
10204
  const unknownMapped = Object.keys(patternMap2).filter(
9955
10205
  (name) => !backup.patterns.some((p) => p.name === name)
9956
10206
  );
@@ -10004,6 +10254,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10004
10254
  if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
10005
10255
  const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
10006
10256
  if (!match) {
10257
+ dictionaryFile(metadata, keywords);
10007
10258
  dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
10008
10259
  continue;
10009
10260
  }
@@ -10044,6 +10295,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10044
10295
  else missingPredefined.push({ name: source.name, candidates });
10045
10296
  continue;
10046
10297
  }
10298
+ if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
10299
+ throw new Error(`Destination data pattern detection technique differs: ${source.name}`);
10047
10300
  patternPlans.push({
10048
10301
  sourceId: source.id,
10049
10302
  name: match2.name,
@@ -10056,6 +10309,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10056
10309
  const target = patternMap2[source.name];
10057
10310
  const match2 = matchByName(destination.patterns, target, retired, "data pattern");
10058
10311
  if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
10312
+ if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
10313
+ throw new Error(`Mapped destination data pattern detection technique differs: ${target}`);
10059
10314
  patternPlans.push({
10060
10315
  sourceId: source.id,
10061
10316
  name: target,
@@ -10164,7 +10419,15 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10164
10419
  const missing = /* @__PURE__ */ new Set();
10165
10420
  for (const leaf of ruleLeaves(item.source)) {
10166
10421
  const id = leaf.value.id;
10167
- if (id != null && id !== "" && !planned.has(String(id))) missing.add(String(id));
10422
+ if (id == null || id === "") continue;
10423
+ if (typeof id !== "string") throw new Error("Backup contains a non-string rule reference ID");
10424
+ const sourceBinding = sourceBindings.get(id);
10425
+ if (sourceBinding) {
10426
+ assertLeafBinding(leaf, sourceBinding);
10427
+ if (leaf.value.version != null && leaf.value.version !== sourceBinding.version)
10428
+ throw new Error("Backup rule pins an unknown source dependency version");
10429
+ }
10430
+ if (!planned.has(id)) missing.add(id);
10168
10431
  }
10169
10432
  if (missing.size) {
10170
10433
  const skippable = [...missing].filter((id) => unresolvedById.has(id));
@@ -10185,20 +10448,39 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10185
10448
  }
10186
10449
  return plan;
10187
10450
  }
10451
+ function assertLeafBinding(leaf, binding) {
10452
+ if (leaf.technique === "dictionary" !== (binding.kind === "dictionary") || leaf.technique !== binding.technique)
10453
+ throw new Error("Data profile rule detection technique does not match its dependency");
10454
+ }
10188
10455
  function planBindings(plan) {
10189
10456
  const bindings = /* @__PURE__ */ new Map();
10190
10457
  for (const item of plan.dictionaries)
10191
- if (item.existing?.id) bindings.set(item.sourceId, { id: item.existing.id, name: item.name });
10458
+ if (item.existing?.id)
10459
+ bindings.set(item.sourceId, {
10460
+ id: item.existing.id,
10461
+ name: item.name,
10462
+ version: 1,
10463
+ kind: "dictionary",
10464
+ technique: "dictionary"
10465
+ });
10192
10466
  for (const item of plan.patterns)
10193
10467
  if (item.existing?.id)
10194
10468
  bindings.set(item.sourceId, {
10195
10469
  id: item.existing.id,
10196
10470
  name: item.name,
10471
+ kind: "pattern",
10472
+ technique: item.existing.detection_config?.technique,
10197
10473
  version: item.existing.version ?? void 0
10198
10474
  });
10199
10475
  return bindings;
10200
10476
  }
10201
10477
  function profileRequest(item, bindings, crossTenant, requireBindings) {
10478
+ if (item.action === "create" && item.name.length > 32)
10479
+ throw new Error("Data profile name exceeds the live API limit of 32 characters");
10480
+ if (item.action === "create" && item.source.profile_type === "basic")
10481
+ throw new Error("Data profile basic writes cannot round-trip through the API");
10482
+ if (item.action === "create" && !item.source.detection_rules?.length)
10483
+ throw new Error("Data profile has no exported detection rules to create");
10202
10484
  const detectionRules = structuredClone(item.source.detection_rules ?? []);
10203
10485
  const known = /* @__PURE__ */ new Set();
10204
10486
  let deferred = false;
@@ -10207,6 +10489,8 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
10207
10489
  known.add(leaf.value);
10208
10490
  const id = leaf.value.id;
10209
10491
  if (id == null || id === "") continue;
10492
+ if (leaf.technique === "dictionary" && leaf.value.version != null && leaf.value.version !== 1)
10493
+ throw new Error("Dictionary rule has an unsupported version pin");
10210
10494
  const binding = bindings.get(String(id));
10211
10495
  if (!binding) {
10212
10496
  if (requireBindings)
@@ -10216,6 +10500,7 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
10216
10500
  deferred = true;
10217
10501
  continue;
10218
10502
  }
10503
+ assertLeafBinding(leaf, binding);
10219
10504
  leaf.value.id = binding.id;
10220
10505
  if (leaf.value.name != null) leaf.value.name = binding.name;
10221
10506
  if (leaf.value.version != null) {
@@ -10254,6 +10539,7 @@ async function restoreDlpResources(api, plan, options = {}) {
10254
10539
  patterns: [],
10255
10540
  profiles: [],
10256
10541
  serverAdded: [],
10542
+ unverifiedCreates: [],
10257
10543
  unresolved: structuredClone(plan.unresolved ?? [])
10258
10544
  };
10259
10545
  const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
@@ -10306,21 +10592,28 @@ async function restoreDlpResources(api, plan, options = {}) {
10306
10592
  if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
10307
10593
  const created = await api.dictionaries.create({
10308
10594
  metadata: item.metadata,
10309
- file: `${item.keywords.join("\n")}
10310
- `,
10595
+ file: dictionaryFile(item.metadata, item.keywords),
10311
10596
  includeKeywords: true
10312
10597
  });
10313
10598
  if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
10599
+ result.unverifiedCreates.push({ kind: "dictionaries", name: item.name, id: created.id });
10314
10600
  const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
10315
10601
  const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
10316
- if (!echo.matches || readBack.name !== item.name)
10602
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name)
10317
10603
  throw new Error(
10318
10604
  `Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10319
10605
  );
10320
10606
  if (echo.serverAdded.length)
10321
10607
  result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
10322
- bindings.set(item.sourceId, { id: created.id, name: item.name });
10608
+ bindings.set(item.sourceId, {
10609
+ id: created.id,
10610
+ name: item.name,
10611
+ version: 1,
10612
+ kind: "dictionary",
10613
+ technique: "dictionary"
10614
+ });
10323
10615
  result.dictionaries.push({ name: item.name, action: "created", id: created.id });
10616
+ result.unverifiedCreates.pop();
10324
10617
  emitItem(
10325
10618
  "dictionaries",
10326
10619
  item.name,
@@ -10348,9 +10641,10 @@ async function restoreDlpResources(api, plan, options = {}) {
10348
10641
  if (!item.body) throw new Error("Data pattern plan is incomplete");
10349
10642
  const created = await api.patterns.create(item.body);
10350
10643
  if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
10644
+ result.unverifiedCreates.push({ kind: "patterns", name: item.name, id: created.id });
10351
10645
  const readBack = await api.patterns.get(created.id);
10352
10646
  const echo = compareEcho(item.body, readBack);
10353
- if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10647
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10354
10648
  throw new Error(
10355
10649
  `Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10356
10650
  );
@@ -10362,9 +10656,12 @@ async function restoreDlpResources(api, plan, options = {}) {
10362
10656
  bindings.set(item.sourceId, {
10363
10657
  id: created.id,
10364
10658
  name: item.name,
10659
+ kind: "pattern",
10660
+ technique: readBack.detection_config?.technique,
10365
10661
  version: readBack.version ?? void 0
10366
10662
  });
10367
10663
  result.patterns.push({ name: item.name, action: "created", id: created.id });
10664
+ result.unverifiedCreates.pop();
10368
10665
  emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
10369
10666
  }
10370
10667
  if (plan.profiles.length)
@@ -10401,9 +10698,10 @@ async function restoreDlpResources(api, plan, options = {}) {
10401
10698
  }
10402
10699
  const created = await api.profiles.create(body);
10403
10700
  if (!created.id) throw new Error(`Restored data profile did not verify: ${item.name}`);
10701
+ result.unverifiedCreates.push({ kind: "profiles", name: item.name, id: created.id });
10404
10702
  const readBack = await api.profiles.get(created.id);
10405
10703
  const echo = compareEcho(body, readBack);
10406
- if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10704
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10407
10705
  throw new Error(
10408
10706
  `Restored data profile did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10409
10707
  );
@@ -10413,6 +10711,7 @@ async function restoreDlpResources(api, plan, options = {}) {
10413
10711
  fields: echo.serverAdded
10414
10712
  });
10415
10713
  result.profiles.push({ name: item.name, action: "created", id: created.id });
10714
+ result.unverifiedCreates.pop();
10416
10715
  emitItem("profiles", item.name, "created", result.profiles.length, plan.profiles.length);
10417
10716
  }
10418
10717
  result.complete = true;
@@ -10421,6 +10720,9 @@ async function restoreDlpResources(api, plan, options = {}) {
10421
10720
  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(
10422
10721
  error.message
10423
10722
  ) ? error.message : "Restore stopped; verify destination state before retrying";
10723
+ const unverified = result.unverifiedCreates[0];
10724
+ if (unverified)
10725
+ result.error += `; unverified create: ${unverified.kind} ${unverified.name} (id ${unverified.id}); inspect this resource before retrying`;
10424
10726
  }
10425
10727
  return result;
10426
10728
  }
@@ -10470,7 +10772,7 @@ function failure(error) {
10470
10772
  if (error instanceof CliUsageError) usageError(error.message);
10471
10773
  if (error instanceof ZodError) {
10472
10774
  ui.error("Invalid DLP backup/request schema; content is not logged.");
10473
- } else if (error instanceof AISecSDKException2) {
10775
+ } else if (error instanceof AISecSDKException4) {
10474
10776
  ui.error(
10475
10777
  `DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
10476
10778
  );
@@ -10544,6 +10846,20 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10544
10846
  for (const entry of rows("unresolved"))
10545
10847
  blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
10546
10848
  }
10849
+ if (rows("unverifiedCreates").length) {
10850
+ blocks.push(heading("Created resources awaiting verification"));
10851
+ blocks.push(
10852
+ formatOutput(
10853
+ rows("unverifiedCreates"),
10854
+ [
10855
+ { key: "kind", label: "Resource" },
10856
+ { key: "name", label: "Name" },
10857
+ { key: "id", label: "Created ID" }
10858
+ ],
10859
+ markdown ? "markdown" : "table"
10860
+ )
10861
+ );
10862
+ }
10547
10863
  if (rows("serverAdded").length) {
10548
10864
  blocks.push(heading("Verified server-added fields (source omitted these)"));
10549
10865
  for (const item of rows("serverAdded"))
@@ -10728,6 +11044,7 @@ function register6(dlp) {
10728
11044
  { key: "profiles", label: "Data profiles" },
10729
11045
  { key: "unresolved", label: "Unresolved references" },
10730
11046
  { key: "serverAdded", label: "Verified server-added fields" },
11047
+ { key: "unverifiedCreates", label: "Created resources awaiting verification" },
10731
11048
  { key: "dryRun", label: "Dry run" },
10732
11049
  { key: "complete", label: "Complete" }
10733
11050
  ];
@@ -10771,7 +11088,7 @@ function registerDlpCommands(runtime) {
10771
11088
  import { randomUUID as randomUUID8 } from "crypto";
10772
11089
  import { readFile as readFile10, stat as stat2 } from "fs/promises";
10773
11090
  import { extname as extname2, resolve as resolve6 } from "path";
10774
- import { AISecSDKException as AISecSDKException3, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
11091
+ import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
10775
11092
  import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
10776
11093
  import { ZodError as ZodError2 } from "zod";
10777
11094
 
@@ -11492,7 +11809,7 @@ function failure2(error) {
11492
11809
  if (error instanceof CliUsageError) usageError(error.message);
11493
11810
  if (error instanceof ZodError2) {
11494
11811
  ui.error("Invalid profile backup/request schema; content is not logged.");
11495
- } else if (error instanceof AISecSDKException3) {
11812
+ } else if (error instanceof AISecSDKException5) {
11496
11813
  ui.error(
11497
11814
  `Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
11498
11815
  );
@@ -13689,12 +14006,41 @@ function buildProgram() {
13689
14006
  const here = dirname5(fileURLToPath(import.meta.url));
13690
14007
  const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
13691
14008
  const program = new Command();
14009
+ let dlpDebugBodyEnvironment;
14010
+ const restoreDlpDebugBody = () => {
14011
+ if (!dlpDebugBodyEnvironment) return;
14012
+ const { previous } = dlpDebugBodyEnvironment;
14013
+ if (previous === void 0) delete process.env.PANW_AI_SEC_DEBUG_BODY;
14014
+ else process.env.PANW_AI_SEC_DEBUG_BODY = previous;
14015
+ dlpDebugBodyEnvironment = void 0;
14016
+ };
14017
+ program.hook("postAction", restoreDlpDebugBody);
14018
+ const parseAsync = program.parseAsync.bind(program);
14019
+ program.parseAsync = async (...args) => {
14020
+ try {
14021
+ return await parseAsync(...args);
14022
+ } finally {
14023
+ restoreDlpDebugBody();
14024
+ }
14025
+ };
13692
14026
  program.name("airs").description(
13693
14027
  "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
13694
- ).version(pkg.version).option("--debug", "Log all AIRS/SCM API requests and responses to a JSONL file").option("--output <format>", "Default output format for read commands").option("--quiet", "Suppress status and decorative output (data and errors still print)");
14028
+ ).version(pkg.version).option("--debug", "Write redacted API diagnostics to a private JSONL file").option("--output <format>", "Default output format for read commands").option("--quiet", "Suppress status and decorative output (data and errors still print)");
13695
14029
  program.hook("preAction", async (_thisCommand, actionCommand) => {
13696
14030
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
13697
14031
  setQuiet(Boolean(root.quiet));
14032
+ let ancestor = actionCommand;
14033
+ let agentGuard = false;
14034
+ let dlp = false;
14035
+ while (ancestor) {
14036
+ if (ancestor.name() === "agentguard") agentGuard = true;
14037
+ if (ancestor.name() === "dlp" && ancestor.parent?.name() === "runtime") dlp = true;
14038
+ ancestor = ancestor.parent;
14039
+ }
14040
+ if (dlp) {
14041
+ dlpDebugBodyEnvironment = { previous: process.env.PANW_AI_SEC_DEBUG_BODY };
14042
+ process.env.PANW_AI_SEC_DEBUG_BODY = "0";
14043
+ }
13698
14044
  const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
13699
14045
  if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
13700
14046
  usageError(
@@ -13721,13 +14067,9 @@ function buildProgram() {
13721
14067
  `debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
13722
14068
  );
13723
14069
  try {
13724
- let ancestor = actionCommand;
13725
- let agentGuard = false;
13726
- while (ancestor) {
13727
- if (ancestor.name() === "agentguard") agentGuard = true;
13728
- ancestor = ancestor.parent;
13729
- }
13730
- installDebugLogger(logPath, { omitBodies: agentGuard });
14070
+ installDebugLogger(logPath, {
14071
+ omitBodies: agentGuard || dlp
14072
+ });
13731
14073
  } catch {
13732
14074
  ui.error(
13733
14075
  "Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cdot65/prisma-airs-cli",
3
3
  "packageManager": "pnpm@10.6.5",
4
- "version": "5.9.0",
4
+ "version": "5.9.1",
5
5
  "description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "license": "MIT",
61
61
  "dependencies": {
62
- "@cdot65/prisma-airs-sdk": "0.30.0",
62
+ "@cdot65/prisma-airs-sdk": "0.30.1",
63
63
  "@inquirer/prompts": "^8.3.0",
64
64
  "chalk": "^5.6.2",
65
65
  "commander": "^14.0.3",