@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.
- package/dist/cli/index.js +144 -18
- 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
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
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
|
-
|
|
10085
|
-
|
|
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) => ({
|
|
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.
|
|
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",
|