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