@cdot65/prisma-airs-cli 5.8.5 → 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 +474 -83
  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 {
@@ -8870,13 +8871,52 @@ async function parseBody(opts) {
8870
8871
  }
8871
8872
  }
8872
8873
 
8874
+ // src/cli/commands/dlp/visibility.ts
8875
+ function predefinedFlag(cmd) {
8876
+ return cmd.option(
8877
+ "--include-predefined",
8878
+ "Include predefined (PANW-shipped) records; listings show only tenant-created records by default"
8879
+ );
8880
+ }
8881
+ function visibleRecords(records, includePredefined) {
8882
+ return includePredefined ? records : records.filter((record) => record.type !== "predefined");
8883
+ }
8884
+
8873
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
+ }
8874
8914
  async function buildMetadata(opts) {
8875
8915
  if (opts.metadataFile) {
8876
- return JSON.parse(await readFile8(opts.metadataFile, "utf-8"));
8916
+ return await readMetadata(opts.metadataFile);
8877
8917
  }
8878
8918
  if (!opts.name || !opts.category || !opts.region || !opts.file) {
8879
- throw new Error("--name, --category, --region, and --file are required");
8919
+ throw new CliUsageError("--name, --category, --region, and --file are required");
8880
8920
  }
8881
8921
  return {
8882
8922
  name: opts.name,
@@ -8894,6 +8934,7 @@ function register(dlp) {
8894
8934
  "Starting offset \u2014 rounds down to a page boundary",
8895
8935
  (v) => Number.parseInt(v, 10)
8896
8936
  ).option("--sort <field,dir>", "(repeatable)", (v, p = []) => [...p, v]).option("--keywords", "Include keyword list in response").option("--include-keywords", "Alias for --keywords").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml");
8937
+ predefinedFlag(listCmd);
8897
8938
  registerPageAliases(listCmd, { sizeFlag: "--size", sizeKey: "size" });
8898
8939
  listCmd.action(async (opts) => {
8899
8940
  try {
@@ -8902,23 +8943,24 @@ function register(dlp) {
8902
8943
  const svc = new SdkDictionariesService(await loadDlpClientOptions());
8903
8944
  const params = {
8904
8945
  size,
8905
- sort: opts.sort,
8946
+ sort: opts.sort ?? ["name,asc"],
8906
8947
  keywords: includeKeywords ? true : void 0
8907
8948
  };
8908
- const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : void 0;
8949
+ const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : (await svc.list({ ...params, page })).content;
8950
+ const visible = visibleRecords(all, opts.includePredefined);
8909
8951
  dlpDictionaries.renderList(
8910
- all ? { content: all, totalElements: all.length } : await svc.list({ ...params, page }),
8952
+ { content: visible, totalElements: visible.length },
8911
8953
  await resolveOutput(listCmd, opts)
8912
8954
  );
8913
8955
  } catch (err) {
8914
8956
  fail(err);
8915
8957
  }
8916
8958
  });
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) => {
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) => {
8918
8960
  try {
8919
8961
  const format = await resolveOutput(command, opts);
8920
8962
  const metadata = await buildMetadata(opts);
8921
- if (!opts.file) throw new Error("--file is required (multipart upload)");
8963
+ if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
8922
8964
  const file = await readFile8(opts.file);
8923
8965
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
8924
8966
  metadata,
@@ -8927,7 +8969,7 @@ function register(dlp) {
8927
8969
  });
8928
8970
  dlpDictionaries.renderCreated(r, format);
8929
8971
  } catch (err) {
8930
- usageError(err instanceof Error ? err.message : String(err));
8972
+ uploadFailure(err);
8931
8973
  }
8932
8974
  });
8933
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) => {
@@ -8945,11 +8987,11 @@ function register(dlp) {
8945
8987
  });
8946
8988
  group.command("replace <id>").description(
8947
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).'
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) => {
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) => {
8949
8991
  try {
8950
8992
  const metadata = await buildMetadata(opts);
8951
8993
  const format = await resolveOutput(command, opts);
8952
- if (!opts.file) throw new Error("--file is required (multipart upload)");
8994
+ if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
8953
8995
  const file = await readFile8(opts.file);
8954
8996
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
8955
8997
  metadata,
@@ -8962,23 +9004,34 @@ function register(dlp) {
8962
9004
  dlpDictionaries.renderReplaced(r, format);
8963
9005
  }
8964
9006
  } catch (err) {
8965
- usageError(err instanceof Error ? err.message : String(err));
9007
+ uploadFailure(err);
8966
9008
  }
8967
9009
  });
8968
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) => {
8969
9011
  try {
8970
9012
  const format = await resolveOutput(command, opts);
8971
9013
  if (opts.bodyFile && (opts.set || opts.clear)) {
8972
- 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
+ }
8973
9027
  }
8974
- const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
8975
9028
  dlpDictionaries.renderPatched(
8976
9029
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
8977
9030
  await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
8978
9031
  format
8979
9032
  );
8980
9033
  } catch (err) {
8981
- usageError(err instanceof Error ? err.message : String(err));
9034
+ dictionaryFailure(err);
8982
9035
  }
8983
9036
  });
8984
9037
  group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
@@ -9095,31 +9148,70 @@ function buildFilteringProfileBody(opts) {
9095
9148
  if (opts.fileType?.length) body.file_type = opts.fileType;
9096
9149
  return body;
9097
9150
  }
9098
- 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) {
9099
9168
  if (!opts.name) throw new Error("--name is required");
9100
- 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);
9101
9180
  const body = {
9102
9181
  name: opts.name,
9103
- profile_type: profileType
9182
+ profile_type: "advanced"
9104
9183
  };
9105
9184
  if (opts.description != null) body.description = opts.description;
9106
9185
  if (opts.granular != null) body.is_granular_data_profile = !!opts.granular;
9107
9186
  if (opts.patternId?.length) {
9108
- const op = (opts.combinator ?? "or").toLowerCase();
9109
- if (!["and", "or", "not", "and_not", "or_not"].includes(op)) {
9110
- throw new Error(`--combinator must be one of and|or|not|and_not|or_not (got '${op}')`);
9111
- }
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
+ });
9112
9209
  body.detection_rules = [
9113
9210
  {
9114
9211
  rule_type: "expression_tree",
9115
9212
  expression_tree: {
9116
- operator_type: op,
9117
- condition_pattern: opts.patternId.map((id) => ({
9118
- data_pattern_id: id,
9119
- confidence_level: opts.confidence ?? "high",
9120
- occurrence_operator_type: "any",
9121
- occurrence_count: 1
9122
- }))
9213
+ operator_type: (opts.combinator ?? "or").toLowerCase(),
9214
+ sub_expressions: leaves
9123
9215
  }
9124
9216
  }
9125
9217
  ];
@@ -9157,8 +9249,8 @@ function register2(dlp) {
9157
9249
  try {
9158
9250
  const { page, size } = resolvePageParams(listCmd, opts);
9159
9251
  const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
9160
- const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9161
- 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"] });
9162
9254
  dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
9163
9255
  } catch (err) {
9164
9256
  fail(err);
@@ -9321,14 +9413,17 @@ async function resolveWriteBody(opts) {
9321
9413
  }
9322
9414
  function register4(dlp) {
9323
9415
  const group = dlp.command("patterns").description("DLP data patterns (full CRUD)");
9324
- const listCmd = listFlags2(group.command("list").description("List data patterns"));
9416
+ const listCmd = predefinedFlag(
9417
+ listFlags2(group.command("list").description("List data patterns (tenant-created by default)"))
9418
+ );
9325
9419
  listCmd.action(async (opts) => {
9326
9420
  try {
9327
9421
  const { page, size } = resolvePageParams(listCmd, opts);
9328
9422
  const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9329
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
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;
9424
+ const visible = visibleRecords(result, opts.includePredefined);
9330
9425
  dlpPatterns.renderList(
9331
- result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
9426
+ { content: visible, totalElements: visible.length },
9332
9427
  await resolveOutput(listCmd, opts)
9333
9428
  );
9334
9429
  } catch (err) {
@@ -9403,6 +9498,9 @@ function register4(dlp) {
9403
9498
  });
9404
9499
  }
9405
9500
 
9501
+ // src/cli/commands/dlp/profiles.ts
9502
+ import { AISecSDKException as AISecSDKException3, ErrorType as ErrorType4 } from "@cdot65/prisma-airs-sdk";
9503
+
9406
9504
  // src/airs/dlp/data-profiles.ts
9407
9505
  var SdkDataProfilesService = class {
9408
9506
  client;
@@ -9440,7 +9538,7 @@ function listFlags3(cmd) {
9440
9538
  return cmd;
9441
9539
  }
9442
9540
  function writeFlags2(cmd) {
9443
- 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(
9444
9542
  "--pattern-id <id>",
9445
9543
  "Data pattern ID to include (repeatable). Builds a simple expression_tree.",
9446
9544
  repeatable
@@ -9452,29 +9550,91 @@ function writeFlags2(cmd) {
9452
9550
  "Raw JSON body file (escape hatch; required for complex rule trees)"
9453
9551
  ).option("--output <fmt>", "Output format", "pretty");
9454
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
+ }
9455
9570
  async function resolveWriteBody2(opts) {
9456
- if (opts.body || opts.bodyFile) {
9457
- const body = await parseBody({
9458
- body: opts.body,
9459
- bodyFile: opts.bodyFile
9460
- });
9461
- 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);
9462
9587
  return body;
9463
9588
  }
9464
- 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
+ }
9465
9622
  }
9466
9623
  function register5(dlp) {
9467
9624
  const group = dlp.command("profiles").description(
9468
9625
  "DLP data profiles. No supported DELETE; status-based retirement is not live-verified."
9469
9626
  );
9470
- const listCmd = listFlags3(group.command("list").description("List data profiles"));
9627
+ const listCmd = predefinedFlag(
9628
+ listFlags3(group.command("list").description("List data profiles (tenant-created by default)"))
9629
+ );
9471
9630
  listCmd.action(async (opts) => {
9472
9631
  try {
9473
9632
  const { page, size } = resolvePageParams(listCmd, opts);
9474
9633
  const svc = new SdkDataProfilesService(await loadDlpClientOptions());
9475
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
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;
9635
+ const visible = visibleRecords(result, opts.includePredefined);
9476
9636
  dlpProfiles.renderList(
9477
- result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
9637
+ { content: visible, totalElements: visible.length },
9478
9638
  await resolveOutput(listCmd, opts)
9479
9639
  );
9480
9640
  } catch (err) {
@@ -9492,7 +9652,7 @@ function register5(dlp) {
9492
9652
  format
9493
9653
  );
9494
9654
  } catch (err) {
9495
- usageError(err instanceof Error ? err.message : String(err));
9655
+ profileFailure(err);
9496
9656
  }
9497
9657
  }
9498
9658
  );
@@ -9517,7 +9677,7 @@ function register5(dlp) {
9517
9677
  format
9518
9678
  );
9519
9679
  } catch (err) {
9520
- usageError(err instanceof Error ? err.message : String(err));
9680
+ profileFailure(err);
9521
9681
  }
9522
9682
  }
9523
9683
  );
@@ -9527,16 +9687,24 @@ function register5(dlp) {
9527
9687
  try {
9528
9688
  const format = await resolveOutput(command, opts);
9529
9689
  if (opts.bodyFile && (opts.set || opts.clear)) {
9530
- throw new Error("--body-file is mutually exclusive with --set/--clear");
9690
+ throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
9531
9691
  }
9532
- 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);
9533
9701
  dlpProfiles.renderPatched(
9534
9702
  // biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
9535
9703
  await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
9536
9704
  format
9537
9705
  );
9538
9706
  } catch (err) {
9539
- usageError(err instanceof Error ? err.message : String(err));
9707
+ profileFailure(err);
9540
9708
  }
9541
9709
  });
9542
9710
  group.command("delete <id>").description("Not supported \u2014 explains the cleanup limitation and exits 2").action((id) => {
@@ -9554,7 +9722,7 @@ function register5(dlp) {
9554
9722
  import { randomUUID as randomUUID7 } from "crypto";
9555
9723
  import { readFile as readFile9, stat } from "fs/promises";
9556
9724
  import { extname, resolve as resolve5 } from "path";
9557
- import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot65/prisma-airs-sdk";
9725
+ import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
9558
9726
  import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
9559
9727
  import { ZodError } from "zod";
9560
9728
 
@@ -9613,13 +9781,79 @@ function assertName(name) {
9613
9781
  async function springPages(fetch, maxPages3, label) {
9614
9782
  pagesLimit(maxPages3);
9615
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
+ };
9616
9797
  for (let page = 0; page < maxPages3; page++) {
9617
9798
  const result = await fetch(page);
9618
- items.push(...result.content);
9619
- 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)
9620
9807
  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;
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
+ }
9623
9857
  if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
9624
9858
  }
9625
9859
  throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
@@ -9627,17 +9861,17 @@ async function springPages(fetch, maxPages3, label) {
9627
9861
  async function inventories(api, maxPages3) {
9628
9862
  return {
9629
9863
  dictionaries: await springPages(
9630
- (page) => api.dictionaries.list({ page, size: 100, keywords: true }),
9864
+ (page) => api.dictionaries.list({ page, size: 100, sort: ["name,asc"], keywords: true }),
9631
9865
  maxPages3,
9632
9866
  "dictionaries"
9633
9867
  ),
9634
9868
  patterns: await springPages(
9635
- (page) => api.patterns.list({ page, size: 100 }),
9869
+ (page) => api.patterns.list({ page, size: 100, sort: ["name,asc"] }),
9636
9870
  maxPages3,
9637
9871
  "data patterns"
9638
9872
  ),
9639
9873
  profiles: await springPages(
9640
- (page) => api.profiles.list({ page, size: 100 }),
9874
+ (page) => api.profiles.list({ page, size: 100, sort: ["name,asc"] }),
9641
9875
  maxPages3,
9642
9876
  "data profiles"
9643
9877
  )
@@ -9752,6 +9986,10 @@ async function backupDlpResources(api, tsgId, options = {}) {
9752
9986
  if (RETIRED_PROFILE.has(profile.profile_status ?? "active")) continue;
9753
9987
  const name = profile.name ?? "(unnamed)";
9754
9988
  try {
9989
+ if (!profile.detection_rules?.length)
9990
+ throw new Error(
9991
+ "has no exported detection rules (the API does not expose basic data profile content)"
9992
+ );
9755
9993
  for (const leaf of ruleLeaves(profile)) {
9756
9994
  const id = leaf.value.id;
9757
9995
  if (id == null || id === "") {
@@ -9763,12 +10001,16 @@ async function backupDlpResources(api, tsgId, options = {}) {
9763
10001
  if (leaf.technique === "dictionary") {
9764
10002
  const record = dictionaryById.get(id);
9765
10003
  if (!record) throw new Error(`references an unknown dictionary: ${id}`);
9766
- if (leaf.value.version != null)
10004
+ if (leaf.value.version != null && leaf.value.version !== 1)
9767
10005
  throw new Error(`references a version-pinned dictionary: ${id}`);
9768
10006
  addDictionary(record);
9769
10007
  } else {
9770
10008
  const record = patternById.get(id);
9771
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");
10012
+ if (RETIRED_PATTERN.has(record.status ?? "active"))
10013
+ throw new Error(`references a retired data pattern: ${record.name ?? id}`);
9772
10014
  if (leaf.value.version != null && record.version !== leaf.value.version)
9773
10015
  throw new Error(
9774
10016
  `references data pattern version ${leaf.value.version}, but the catalog holds version ${record.version ?? "unknown"}: ${id}`
@@ -9822,6 +10064,10 @@ function compareEcho(request, readBack) {
9822
10064
  differences.push(path3);
9823
10065
  return;
9824
10066
  }
10067
+ if (path3.endsWith(".supported_confidence_levels") && Array.isArray(a) && Array.isArray(b) && a.every((level) => b.includes(level))) {
10068
+ if (a.length !== b.length) serverAdded.push(path3);
10069
+ return;
10070
+ }
9825
10071
  if (Array.isArray(a) || Array.isArray(b)) {
9826
10072
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
9827
10073
  differences.push(path3);
@@ -9849,6 +10095,20 @@ function keywordSet(value) {
9849
10095
  return void 0;
9850
10096
  return [...value].sort();
9851
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
+ }
9852
10112
  function dictionaryEcho(metadata, record, keywords) {
9853
10113
  const { original_file_name, ...projection } = metadata;
9854
10114
  const echo = compareEcho(projection, record);
@@ -9864,7 +10124,8 @@ function dictionaryRequest(source, name) {
9864
10124
  original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
9865
10125
  region_name: source.region_name,
9866
10126
  ...source.description != null ? { description: source.description } : {},
9867
- ...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) } : {}
9868
10129
  };
9869
10130
  const parsed = DictionaryRequestSchema.safeParse(metadata);
9870
10131
  if (!parsed.success)
@@ -9922,6 +10183,24 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9922
10183
  uniqueNames(backup.dictionaries, "dictionary");
9923
10184
  uniqueNames(backup.patterns, "data pattern");
9924
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
+ }
9925
10204
  const unknownMapped = Object.keys(patternMap2).filter(
9926
10205
  (name) => !backup.patterns.some((p) => p.name === name)
9927
10206
  );
@@ -9975,6 +10254,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9975
10254
  if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
9976
10255
  const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
9977
10256
  if (!match) {
10257
+ dictionaryFile(metadata, keywords);
9978
10258
  dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
9979
10259
  continue;
9980
10260
  }
@@ -10015,6 +10295,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10015
10295
  else missingPredefined.push({ name: source.name, candidates });
10016
10296
  continue;
10017
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}`);
10018
10300
  patternPlans.push({
10019
10301
  sourceId: source.id,
10020
10302
  name: match2.name,
@@ -10027,6 +10309,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10027
10309
  const target = patternMap2[source.name];
10028
10310
  const match2 = matchByName(destination.patterns, target, retired, "data pattern");
10029
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}`);
10030
10314
  patternPlans.push({
10031
10315
  sourceId: source.id,
10032
10316
  name: target,
@@ -10049,6 +10333,20 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10049
10333
  `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>"`
10050
10334
  );
10051
10335
  }
10336
+ if (["regex", "weighted_regex"].includes(source.detection_config?.technique ?? "") && !(Array.isArray(source.matching_rules?.regexes) && source.matching_rules.regexes.length)) {
10337
+ if (options.skipUnresolved) {
10338
+ unresolved.push({
10339
+ kind: "pattern",
10340
+ sourceId: source.id,
10341
+ name: source.name,
10342
+ reason: "detection content is not exported by the API (copy of a predefined pattern)"
10343
+ });
10344
+ continue;
10345
+ }
10346
+ throw new Error(
10347
+ `Data pattern has no exported detection content (a copy of a predefined pattern): ${source.name}; bind an equivalent with --pattern-map "${source.name}=<destination-name>" or exclude dependents with --skip-unresolved`
10348
+ );
10349
+ }
10052
10350
  const name = `${prefix}${source.name}`;
10053
10351
  assertName(name);
10054
10352
  const body = patternRequest(source, name);
@@ -10121,7 +10419,15 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10121
10419
  const missing = /* @__PURE__ */ new Set();
10122
10420
  for (const leaf of ruleLeaves(item.source)) {
10123
10421
  const id = leaf.value.id;
10124
- 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);
10125
10431
  }
10126
10432
  if (missing.size) {
10127
10433
  const skippable = [...missing].filter((id) => unresolvedById.has(id));
@@ -10142,20 +10448,39 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10142
10448
  }
10143
10449
  return plan;
10144
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
+ }
10145
10455
  function planBindings(plan) {
10146
10456
  const bindings = /* @__PURE__ */ new Map();
10147
10457
  for (const item of plan.dictionaries)
10148
- 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
+ });
10149
10466
  for (const item of plan.patterns)
10150
10467
  if (item.existing?.id)
10151
10468
  bindings.set(item.sourceId, {
10152
10469
  id: item.existing.id,
10153
10470
  name: item.name,
10471
+ kind: "pattern",
10472
+ technique: item.existing.detection_config?.technique,
10154
10473
  version: item.existing.version ?? void 0
10155
10474
  });
10156
10475
  return bindings;
10157
10476
  }
10158
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");
10159
10484
  const detectionRules = structuredClone(item.source.detection_rules ?? []);
10160
10485
  const known = /* @__PURE__ */ new Set();
10161
10486
  let deferred = false;
@@ -10164,6 +10489,8 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
10164
10489
  known.add(leaf.value);
10165
10490
  const id = leaf.value.id;
10166
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");
10167
10494
  const binding = bindings.get(String(id));
10168
10495
  if (!binding) {
10169
10496
  if (requireBindings)
@@ -10173,6 +10500,7 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
10173
10500
  deferred = true;
10174
10501
  continue;
10175
10502
  }
10503
+ assertLeafBinding(leaf, binding);
10176
10504
  leaf.value.id = binding.id;
10177
10505
  if (leaf.value.name != null) leaf.value.name = binding.name;
10178
10506
  if (leaf.value.version != null) {
@@ -10211,6 +10539,7 @@ async function restoreDlpResources(api, plan, options = {}) {
10211
10539
  patterns: [],
10212
10540
  profiles: [],
10213
10541
  serverAdded: [],
10542
+ unverifiedCreates: [],
10214
10543
  unresolved: structuredClone(plan.unresolved ?? [])
10215
10544
  };
10216
10545
  const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
@@ -10263,19 +10592,28 @@ async function restoreDlpResources(api, plan, options = {}) {
10263
10592
  if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
10264
10593
  const created = await api.dictionaries.create({
10265
10594
  metadata: item.metadata,
10266
- file: `${item.keywords.join("\n")}
10267
- `,
10595
+ file: dictionaryFile(item.metadata, item.keywords),
10268
10596
  includeKeywords: true
10269
10597
  });
10270
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 });
10271
10600
  const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
10272
10601
  const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
10273
- if (!echo.matches || readBack.name !== item.name)
10274
- throw new Error(`Restored dictionary did not verify: ${item.name}`);
10602
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name)
10603
+ throw new Error(
10604
+ `Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10605
+ );
10275
10606
  if (echo.serverAdded.length)
10276
10607
  result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
10277
- 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
+ });
10278
10615
  result.dictionaries.push({ name: item.name, action: "created", id: created.id });
10616
+ result.unverifiedCreates.pop();
10279
10617
  emitItem(
10280
10618
  "dictionaries",
10281
10619
  item.name,
@@ -10303,10 +10641,13 @@ async function restoreDlpResources(api, plan, options = {}) {
10303
10641
  if (!item.body) throw new Error("Data pattern plan is incomplete");
10304
10642
  const created = await api.patterns.create(item.body);
10305
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 });
10306
10645
  const readBack = await api.patterns.get(created.id);
10307
10646
  const echo = compareEcho(item.body, readBack);
10308
- if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10309
- throw new Error(`Restored data pattern did not verify: ${item.name}`);
10647
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10648
+ throw new Error(
10649
+ `Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10650
+ );
10310
10651
  if (echo.serverAdded.length)
10311
10652
  result.serverAdded.push({
10312
10653
  resource: `data pattern ${item.name}`,
@@ -10315,9 +10656,12 @@ async function restoreDlpResources(api, plan, options = {}) {
10315
10656
  bindings.set(item.sourceId, {
10316
10657
  id: created.id,
10317
10658
  name: item.name,
10659
+ kind: "pattern",
10660
+ technique: readBack.detection_config?.technique,
10318
10661
  version: readBack.version ?? void 0
10319
10662
  });
10320
10663
  result.patterns.push({ name: item.name, action: "created", id: created.id });
10664
+ result.unverifiedCreates.pop();
10321
10665
  emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
10322
10666
  }
10323
10667
  if (plan.profiles.length)
@@ -10354,16 +10698,20 @@ async function restoreDlpResources(api, plan, options = {}) {
10354
10698
  }
10355
10699
  const created = await api.profiles.create(body);
10356
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 });
10357
10702
  const readBack = await api.profiles.get(created.id);
10358
10703
  const echo = compareEcho(body, readBack);
10359
- if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10360
- throw new Error(`Restored data profile did not verify: ${item.name}`);
10704
+ if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10705
+ throw new Error(
10706
+ `Restored data profile did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10707
+ );
10361
10708
  if (echo.serverAdded.length)
10362
10709
  result.serverAdded.push({
10363
10710
  resource: `data profile ${item.name}`,
10364
10711
  fields: echo.serverAdded
10365
10712
  });
10366
10713
  result.profiles.push({ name: item.name, action: "created", id: created.id });
10714
+ result.unverifiedCreates.pop();
10367
10715
  emitItem("profiles", item.name, "created", result.profiles.length, plan.profiles.length);
10368
10716
  }
10369
10717
  result.complete = true;
@@ -10372,6 +10720,9 @@ async function restoreDlpResources(api, plan, options = {}) {
10372
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(
10373
10721
  error.message
10374
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`;
10375
10726
  }
10376
10727
  return result;
10377
10728
  }
@@ -10421,7 +10772,7 @@ function failure(error) {
10421
10772
  if (error instanceof CliUsageError) usageError(error.message);
10422
10773
  if (error instanceof ZodError) {
10423
10774
  ui.error("Invalid DLP backup/request schema; content is not logged.");
10424
- } else if (error instanceof AISecSDKException2) {
10775
+ } else if (error instanceof AISecSDKException4) {
10425
10776
  ui.error(
10426
10777
  `DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
10427
10778
  );
@@ -10495,6 +10846,20 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10495
10846
  for (const entry of rows("unresolved"))
10496
10847
  blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
10497
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
+ }
10498
10863
  if (rows("serverAdded").length) {
10499
10864
  blocks.push(heading("Verified server-added fields (source omitted these)"));
10500
10865
  for (const item of rows("serverAdded"))
@@ -10679,6 +11044,7 @@ function register6(dlp) {
10679
11044
  { key: "profiles", label: "Data profiles" },
10680
11045
  { key: "unresolved", label: "Unresolved references" },
10681
11046
  { key: "serverAdded", label: "Verified server-added fields" },
11047
+ { key: "unverifiedCreates", label: "Created resources awaiting verification" },
10682
11048
  { key: "dryRun", label: "Dry run" },
10683
11049
  { key: "complete", label: "Complete" }
10684
11050
  ];
@@ -10722,7 +11088,7 @@ function registerDlpCommands(runtime) {
10722
11088
  import { randomUUID as randomUUID8 } from "crypto";
10723
11089
  import { readFile as readFile10, stat as stat2 } from "fs/promises";
10724
11090
  import { extname as extname2, resolve as resolve6 } from "path";
10725
- 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";
10726
11092
  import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
10727
11093
  import { ZodError as ZodError2 } from "zod";
10728
11094
 
@@ -11443,7 +11809,7 @@ function failure2(error) {
11443
11809
  if (error instanceof CliUsageError) usageError(error.message);
11444
11810
  if (error instanceof ZodError2) {
11445
11811
  ui.error("Invalid profile backup/request schema; content is not logged.");
11446
- } else if (error instanceof AISecSDKException3) {
11812
+ } else if (error instanceof AISecSDKException5) {
11447
11813
  ui.error(
11448
11814
  `Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
11449
11815
  );
@@ -13640,12 +14006,41 @@ function buildProgram() {
13640
14006
  const here = dirname5(fileURLToPath(import.meta.url));
13641
14007
  const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
13642
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
+ };
13643
14026
  program.name("airs").description(
13644
14027
  "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
13645
- ).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)");
13646
14029
  program.hook("preAction", async (_thisCommand, actionCommand) => {
13647
14030
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
13648
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
+ }
13649
14044
  const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
13650
14045
  if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
13651
14046
  usageError(
@@ -13672,13 +14067,9 @@ function buildProgram() {
13672
14067
  `debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
13673
14068
  );
13674
14069
  try {
13675
- let ancestor = actionCommand;
13676
- let agentGuard = false;
13677
- while (ancestor) {
13678
- if (ancestor.name() === "agentguard") agentGuard = true;
13679
- ancestor = ancestor.parent;
13680
- }
13681
- installDebugLogger(logPath, { omitBodies: agentGuard });
14070
+ installDebugLogger(logPath, {
14071
+ omitBodies: agentGuard || dlp
14072
+ });
13682
14073
  } catch {
13683
14074
  ui.error(
13684
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.8.5",
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",