@cdot65/prisma-airs-cli 5.8.3 → 5.8.4

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 +144 -18
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -9710,6 +9710,7 @@ async function backupDlpResources(api, tsgId, options = {}) {
9710
9710
  const kinds = options.resources ?? [...DLP_RESOURCE_KINDS];
9711
9711
  if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)))
9712
9712
  throw new Error("resources must name dictionaries, patterns and/or profiles");
9713
+ options.onProgress?.({ phase: "inventories" });
9713
9714
  const inventory2 = await inventories(api, maxPages3);
9714
9715
  tenantIdentity([...inventory2.patterns, ...inventory2.profiles]);
9715
9716
  const dictionaries = /* @__PURE__ */ new Map();
@@ -9918,6 +9919,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9918
9919
  if (unknownMapped.length)
9919
9920
  throw new Error(`--pattern-map names absent from the backup: ${unknownMapped.join(", ")}`);
9920
9921
  const crossTenant = backup.source.tsgId !== destinationTsgId;
9922
+ const unresolved = [];
9923
+ options.onProgress?.({ phase: "inventories" });
9921
9924
  const destination = await inventories(api, maxPages3);
9922
9925
  const sourceTenant = tenantIdentity([...backup.patterns, ...backup.profiles]);
9923
9926
  const destinationTenant = tenantIdentity([...destination.patterns, ...destination.profiles]);
@@ -9936,8 +9939,18 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9936
9939
  if (!source.id || !source.name) throw new Error("Backup dictionary lacks identity");
9937
9940
  if (source.type === "predefined") {
9938
9941
  const match2 = matchByName(destination.dictionaries, source.name, () => false, "dictionary");
9939
- if (!match2?.id || match2.type !== "predefined")
9942
+ if (!match2?.id || match2.type !== "predefined") {
9943
+ if (options.skipUnresolved) {
9944
+ unresolved.push({
9945
+ kind: "dictionary",
9946
+ sourceId: source.id,
9947
+ name: source.name,
9948
+ reason: "missing predefined destination dictionary"
9949
+ });
9950
+ continue;
9951
+ }
9940
9952
  throw new Error(`Missing predefined destination dictionary: ${source.name}`);
9953
+ }
9941
9954
  dictionaryPlans.push({
9942
9955
  sourceId: source.id,
9943
9956
  name: source.name,
@@ -9981,10 +9994,16 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
9981
9994
  );
9982
9995
  const match2 = byId ?? matchByName(destination.patterns, source.name, retired, "data pattern");
9983
9996
  if (!match2?.id || !match2.name || match2.type !== "predefined") {
9984
- missingPredefined.push({
9985
- name: source.name,
9986
- candidates: predefinedCandidates(source, destination.patterns)
9987
- });
9997
+ const candidates = predefinedCandidates(source, destination.patterns);
9998
+ if (options.skipUnresolved)
9999
+ unresolved.push({
10000
+ kind: "pattern",
10001
+ sourceId: source.id,
10002
+ name: source.name,
10003
+ reason: "no identity or name match in the destination catalog",
10004
+ candidates
10005
+ });
10006
+ else missingPredefined.push({ name: source.name, candidates });
9988
10007
  continue;
9989
10008
  }
9990
10009
  patternPlans.push({
@@ -10007,10 +10026,20 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10007
10026
  });
10008
10027
  continue;
10009
10028
  }
10010
- if (!portablePattern(source))
10029
+ if (!portablePattern(source)) {
10030
+ if (options.skipUnresolved) {
10031
+ unresolved.push({
10032
+ kind: "pattern",
10033
+ sourceId: source.id,
10034
+ name: source.name,
10035
+ reason: "tenant-bound detection technique without --pattern-map"
10036
+ });
10037
+ continue;
10038
+ }
10011
10039
  throw new Error(
10012
10040
  `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>"`
10013
10041
  );
10042
+ }
10014
10043
  const name = `${prefix}${source.name}`;
10015
10044
  assertName(name);
10016
10045
  const body = patternRequest(source, name);
@@ -10069,6 +10098,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10069
10098
  dictionaries: dictionaryPlans,
10070
10099
  patterns: patternPlans,
10071
10100
  profiles: profilePlans,
10101
+ unresolved,
10072
10102
  maxPages: maxPages3
10073
10103
  };
10074
10104
  const bindings = planBindings(plan);
@@ -10076,14 +10106,24 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10076
10106
  ...dictionaryPlans.map((d) => d.sourceId),
10077
10107
  ...patternPlans.map((p) => p.sourceId)
10078
10108
  ]);
10109
+ const unresolvedById = new Map(unresolved.map((item) => [item.sourceId, item]));
10079
10110
  for (const item of profilePlans) {
10080
10111
  if (item.action === "skip") continue;
10112
+ const missing = /* @__PURE__ */ new Set();
10081
10113
  for (const leaf of ruleLeaves(item.source)) {
10082
10114
  const id = leaf.value.id;
10083
- if (id != null && id !== "" && !planned.has(String(id)))
10084
- throw new Error(
10085
- `Backup data profile references a resource absent from the backup: ${item.name}`
10086
- );
10115
+ if (id != null && id !== "" && !planned.has(String(id))) missing.add(String(id));
10116
+ }
10117
+ if (missing.size) {
10118
+ const skippable = [...missing].filter((id) => unresolvedById.has(id));
10119
+ if (skippable.length === missing.size) {
10120
+ item.action = "skip";
10121
+ item.reason = `unresolved references: ${skippable.map((id) => unresolvedById.get(id)?.name).join(", ")}`;
10122
+ continue;
10123
+ }
10124
+ throw new Error(
10125
+ `Backup data profile references a resource absent from the backup: ${item.name}`
10126
+ );
10087
10127
  }
10088
10128
  const body = profileRequest(item, bindings, crossTenant, item.action === "verify");
10089
10129
  if (item.action === "verify" && body && !compareEcho(body, item.existing).matches)
@@ -10153,7 +10193,7 @@ function assertUnchanged(current, prior, label) {
10153
10193
  if (canonical(current ?? null) !== canonical(prior ?? null))
10154
10194
  throw new Error(`Destination ${label} changed after planning`);
10155
10195
  }
10156
- async function restoreDlpResources(api, plan) {
10196
+ async function restoreDlpResources(api, plan, options = {}) {
10157
10197
  const result = {
10158
10198
  sourceTsgId: plan.sourceTsgId,
10159
10199
  destinationTsgId: plan.destinationTsgId,
@@ -10161,10 +10201,14 @@ async function restoreDlpResources(api, plan) {
10161
10201
  dictionaries: [],
10162
10202
  patterns: [],
10163
10203
  profiles: [],
10164
- serverAdded: []
10204
+ serverAdded: [],
10205
+ unresolved: structuredClone(plan.unresolved ?? [])
10165
10206
  };
10166
10207
  const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
10208
+ const emit2 = options.onProgress;
10209
+ const emitItem = (stage, name, action, index, total) => emit2?.({ phase: "item", stage, name, action, index, total });
10167
10210
  try {
10211
+ emit2?.({ phase: "recheck" });
10168
10212
  const current = await inventories(api, plan.maxPages);
10169
10213
  for (const item of plan.dictionaries) {
10170
10214
  const match = matchByName(current.dictionaries, item.name, () => false, "dictionary");
@@ -10191,11 +10235,20 @@ async function restoreDlpResources(api, plan) {
10191
10235
  throw new Error(`Destination data profile changed after planning: ${item.name}`);
10192
10236
  }
10193
10237
  const bindings = planBindings(plan);
10238
+ if (plan.dictionaries.length)
10239
+ emit2?.({ phase: "stage", stage: "dictionaries", total: plan.dictionaries.length });
10194
10240
  for (const item of plan.dictionaries) {
10195
10241
  if (item.action !== "create") {
10196
10242
  const id = item.existing?.id;
10197
10243
  if (!id) throw new Error(`Destination dictionary has no identity: ${item.name}`);
10198
10244
  result.dictionaries.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
10245
+ emitItem(
10246
+ "dictionaries",
10247
+ item.name,
10248
+ DEPENDENCY_ACTIONS[item.action],
10249
+ result.dictionaries.length,
10250
+ plan.dictionaries.length
10251
+ );
10199
10252
  continue;
10200
10253
  }
10201
10254
  if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
@@ -10214,12 +10267,28 @@ async function restoreDlpResources(api, plan) {
10214
10267
  result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
10215
10268
  bindings.set(item.sourceId, { id: created.id, name: item.name });
10216
10269
  result.dictionaries.push({ name: item.name, action: "created", id: created.id });
10270
+ emitItem(
10271
+ "dictionaries",
10272
+ item.name,
10273
+ "created",
10274
+ result.dictionaries.length,
10275
+ plan.dictionaries.length
10276
+ );
10217
10277
  }
10278
+ if (plan.patterns.length)
10279
+ emit2?.({ phase: "stage", stage: "patterns", total: plan.patterns.length });
10218
10280
  for (const item of plan.patterns) {
10219
10281
  if (item.action !== "create") {
10220
10282
  const id = item.existing?.id;
10221
10283
  if (!id) throw new Error(`Destination data pattern has no identity: ${item.name}`);
10222
10284
  result.patterns.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
10285
+ emitItem(
10286
+ "patterns",
10287
+ item.name,
10288
+ DEPENDENCY_ACTIONS[item.action],
10289
+ result.patterns.length,
10290
+ plan.patterns.length
10291
+ );
10223
10292
  continue;
10224
10293
  }
10225
10294
  if (!item.body) throw new Error("Data pattern plan is incomplete");
@@ -10240,14 +10309,19 @@ async function restoreDlpResources(api, plan) {
10240
10309
  version: readBack.version ?? void 0
10241
10310
  });
10242
10311
  result.patterns.push({ name: item.name, action: "created", id: created.id });
10312
+ emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
10243
10313
  }
10314
+ if (plan.profiles.length)
10315
+ emit2?.({ phase: "stage", stage: "profiles", total: plan.profiles.length });
10244
10316
  for (const item of plan.profiles) {
10245
10317
  if (item.action === "skip") {
10246
10318
  result.profiles.push({
10247
10319
  name: item.name,
10248
10320
  action: "skipped",
10249
- id: item.existing?.id ?? void 0
10321
+ id: item.existing?.id ?? void 0,
10322
+ ...item.reason ? { reason: item.reason } : {}
10250
10323
  });
10324
+ emitItem("profiles", item.name, "skipped", result.profiles.length, plan.profiles.length);
10251
10325
  continue;
10252
10326
  }
10253
10327
  const body = profileRequest(item, bindings, crossTenant, true);
@@ -10266,6 +10340,7 @@ async function restoreDlpResources(api, plan) {
10266
10340
  action: "verified",
10267
10341
  id: item.existing?.id ?? void 0
10268
10342
  });
10343
+ emitItem("profiles", item.name, "verified", result.profiles.length, plan.profiles.length);
10269
10344
  continue;
10270
10345
  }
10271
10346
  const created = await api.profiles.create(body);
@@ -10280,6 +10355,7 @@ async function restoreDlpResources(api, plan) {
10280
10355
  fields: echo.serverAdded
10281
10356
  });
10282
10357
  result.profiles.push({ name: item.name, action: "created", id: created.id });
10358
+ emitItem("profiles", item.name, "created", result.profiles.length, plan.profiles.length);
10283
10359
  }
10284
10360
  result.complete = true;
10285
10361
  } catch (error) {
@@ -10349,6 +10425,31 @@ function failure(error) {
10349
10425
  }
10350
10426
  process.exitCode = 1;
10351
10427
  }
10428
+ var STAGE_LABELS = {
10429
+ dictionaries: "dictionaries",
10430
+ patterns: "data patterns",
10431
+ profiles: "data profiles"
10432
+ };
10433
+ var ITEM_LABELS = {
10434
+ dictionaries: "dictionary",
10435
+ patterns: "data pattern",
10436
+ profiles: "data profile"
10437
+ };
10438
+ function progressRenderer(format) {
10439
+ if (format !== "pretty") return void 0;
10440
+ return (event) => {
10441
+ if (event.phase === "inventories")
10442
+ ui.info("Reading inventories (dictionaries, data patterns, data profiles)\u2026");
10443
+ else if (event.phase === "recheck") ui.info("Rechecking destination state before writing\u2026");
10444
+ else if (event.phase === "stage")
10445
+ ui.info(`Restoring ${STAGE_LABELS[event.stage]} (${event.total})\u2026`);
10446
+ else
10447
+ ui.bullet(
10448
+ `${ITEM_LABELS[event.stage]} ${event.action}: ${event.name} (${event.index}/${event.total})`,
10449
+ event.action === "skipped" ? "skip" : "success"
10450
+ );
10451
+ };
10452
+ }
10352
10453
  function renderRestore(value, columns, format) {
10353
10454
  if (format !== "pretty" && format !== "table" && format !== "markdown")
10354
10455
  return formatOutput([value], columns, format);
@@ -10380,6 +10481,11 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
10380
10481
  ) || "(none)"
10381
10482
  );
10382
10483
  }
10484
+ if (rows("unresolved").length) {
10485
+ blocks.push(heading("Unresolved references \u2014 dependent profiles skipped"));
10486
+ for (const entry of rows("unresolved"))
10487
+ blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
10488
+ }
10383
10489
  if (rows("serverAdded").length) {
10384
10490
  blocks.push(heading("Verified server-added fields (source omitted these)"));
10385
10491
  for (const item of rows("serverAdded"))
@@ -10432,7 +10538,8 @@ function register6(dlp) {
10432
10538
  const { backup: data, skipped } = await backupDlpResources(api, tsgId, {
10433
10539
  resources: opts.resources,
10434
10540
  maxPages: opts.maxPages,
10435
- skipUnsupported: Boolean(opts.skipUnsupported)
10541
+ skipUnsupported: Boolean(opts.skipUnsupported),
10542
+ onProgress: progressRenderer(format)
10436
10543
  });
10437
10544
  for (const item of skipped)
10438
10545
  ui.warning(`Skipped unsupported data profile: ${item.profile} (${item.reason})`);
@@ -10476,9 +10583,12 @@ function register6(dlp) {
10476
10583
  "error"
10477
10584
  ).option(
10478
10585
  "--pattern-map <source=destination>",
10479
- "Bind a tenant-bound source pattern to an existing destination pattern (repeatable)",
10586
+ "Bind a tenant-bound or predefined source pattern to an existing destination pattern (repeatable)",
10480
10587
  (value, prior) => [...prior, value],
10481
10588
  []
10589
+ ).option(
10590
+ "--skip-unresolved",
10591
+ "Skip references with no destination match, and every profile depending on them; whole profiles only, each reported"
10482
10592
  ).option(
10483
10593
  "--max-pages <n>",
10484
10594
  "Maximum pages per inventory; incomplete inventories fail",
@@ -10523,19 +10633,34 @@ function register6(dlp) {
10523
10633
  throw new CliUsageError(
10524
10634
  "--expect-tsg does not match the selected destination tenant; no API request was made"
10525
10635
  );
10636
+ const onProgress = progressRenderer(format);
10526
10637
  const plan = await planDlpResourcesRestore(api, data, tsgId, {
10527
10638
  namePrefix: opts.namePrefix,
10528
10639
  onConflict: opts.onConflict,
10529
10640
  patternMap: mappings,
10530
- maxPages: opts.maxPages
10641
+ maxPages: opts.maxPages,
10642
+ skipUnresolved: Boolean(opts.skipUnresolved),
10643
+ onProgress
10531
10644
  });
10645
+ for (const entry of plan.unresolved)
10646
+ ui.warning(
10647
+ `Unresolved ${entry.kind === "dictionary" ? "dictionary" : "data pattern"}: ${entry.name} (${entry.reason})${entry.candidates?.length ? `; candidates: ${entry.candidates.map((name) => `"${name}"`).join(", ")}` : ""}`
10648
+ );
10649
+ for (const profile of plan.profiles)
10650
+ if (profile.reason)
10651
+ ui.warning(`Skipping data profile: ${profile.name} (${profile.reason})`);
10532
10652
  const summary = {
10533
10653
  sourceTsgId: plan.sourceTsgId,
10534
10654
  destinationTsgId: plan.destinationTsgId,
10535
10655
  dryRun: Boolean(opts.dryRun),
10536
10656
  dictionaries: plan.dictionaries.map((d) => ({ name: d.name, action: d.action })),
10537
10657
  patterns: plan.patterns.map((p) => ({ name: p.name, action: p.action })),
10538
- profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action }))
10658
+ profiles: plan.profiles.map((p) => ({
10659
+ name: p.name,
10660
+ action: p.action,
10661
+ ...p.reason ? { reason: p.reason } : {}
10662
+ })),
10663
+ unresolved: plan.unresolved.map(({ kind, name, reason }) => ({ kind, name, reason }))
10539
10664
  };
10540
10665
  const columns = [
10541
10666
  { key: "sourceTsgId", label: "Source TSG" },
@@ -10543,6 +10668,7 @@ function register6(dlp) {
10543
10668
  { key: "dictionaries", label: "Dictionaries" },
10544
10669
  { key: "patterns", label: "Data patterns" },
10545
10670
  { key: "profiles", label: "Data profiles" },
10671
+ { key: "unresolved", label: "Unresolved references" },
10546
10672
  { key: "serverAdded", label: "Verified server-added fields" },
10547
10673
  { key: "dryRun", label: "Dry run" },
10548
10674
  { key: "complete", label: "Complete" }
@@ -10558,7 +10684,7 @@ function register6(dlp) {
10558
10684
  Boolean(opts.force),
10559
10685
  { action: `restore DLP resources into TSG ${tsgId}` }
10560
10686
  );
10561
- const result = await restoreDlpResources(api, plan);
10687
+ const result = await restoreDlpResources(api, plan, { onProgress });
10562
10688
  console.log(renderRestore({ ...result }, columns, format));
10563
10689
  if (!result.complete) {
10564
10690
  ui.error(result.error ?? "Restore incomplete");
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.3",
4
+ "version": "5.8.4",
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",