@cdot65/prisma-airs-cli 5.6.0 → 5.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +417 -113
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -522,6 +522,10 @@ ${INDENT}${chalk3.bold(label)}
|
|
|
522
522
|
warn(msg) {
|
|
523
523
|
console.log(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
|
|
524
524
|
},
|
|
525
|
+
/** Safety warnings remain visible in quiet mode without corrupting structured stdout. */
|
|
526
|
+
warning(msg) {
|
|
527
|
+
console.error(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
|
|
528
|
+
},
|
|
525
529
|
/** Neutral informational callout. Quiet-suppressed. */
|
|
526
530
|
info(msg) {
|
|
527
531
|
if (quietMode) return;
|
|
@@ -8800,6 +8804,11 @@ var SdkDictionariesService = class {
|
|
|
8800
8804
|
}
|
|
8801
8805
|
};
|
|
8802
8806
|
|
|
8807
|
+
// src/cli/commands/dlp/config.ts
|
|
8808
|
+
async function loadDlpClientOptions() {
|
|
8809
|
+
return managementClientOptions(await loadConfig());
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8803
8812
|
// src/cli/commands/dlp/patch.ts
|
|
8804
8813
|
import { readFile as readFile7 } from "fs/promises";
|
|
8805
8814
|
function buildMergePatch(opts) {
|
|
@@ -8890,7 +8899,7 @@ function register(dlp) {
|
|
|
8890
8899
|
try {
|
|
8891
8900
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
8892
8901
|
const includeKeywords = opts.keywords || opts.includeKeywords;
|
|
8893
|
-
const svc = new SdkDictionariesService();
|
|
8902
|
+
const svc = new SdkDictionariesService(await loadDlpClientOptions());
|
|
8894
8903
|
const params = {
|
|
8895
8904
|
size,
|
|
8896
8905
|
sort: opts.sort,
|
|
@@ -8905,17 +8914,18 @@ function register(dlp) {
|
|
|
8905
8914
|
fail(err);
|
|
8906
8915
|
}
|
|
8907
8916
|
});
|
|
8908
|
-
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) => {
|
|
8909
8918
|
try {
|
|
8919
|
+
const format = await resolveOutput(command, opts);
|
|
8910
8920
|
const metadata = await buildMetadata(opts);
|
|
8911
8921
|
if (!opts.file) throw new Error("--file is required (multipart upload)");
|
|
8912
8922
|
const file = await readFile8(opts.file);
|
|
8913
|
-
const r = await new SdkDictionariesService().create({
|
|
8923
|
+
const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
|
|
8914
8924
|
metadata,
|
|
8915
8925
|
file,
|
|
8916
8926
|
includeKeywords: opts.includeKeywords
|
|
8917
8927
|
});
|
|
8918
|
-
dlpDictionaries.renderCreated(r,
|
|
8928
|
+
dlpDictionaries.renderCreated(r, format);
|
|
8919
8929
|
} catch (err) {
|
|
8920
8930
|
usageError(err instanceof Error ? err.message : String(err));
|
|
8921
8931
|
}
|
|
@@ -8924,7 +8934,9 @@ function register(dlp) {
|
|
|
8924
8934
|
try {
|
|
8925
8935
|
const includeKeywords = opts.keywords || opts.includeKeywords;
|
|
8926
8936
|
dlpDictionaries.renderGet(
|
|
8927
|
-
await new SdkDictionariesService().get(id, {
|
|
8937
|
+
await new SdkDictionariesService(await loadDlpClientOptions()).get(id, {
|
|
8938
|
+
includeKeywords
|
|
8939
|
+
}),
|
|
8928
8940
|
await resolveOutput(getCmd, opts)
|
|
8929
8941
|
);
|
|
8930
8942
|
} catch (err) {
|
|
@@ -8933,12 +8945,13 @@ function register(dlp) {
|
|
|
8933
8945
|
});
|
|
8934
8946
|
group.command("replace <id>").description(
|
|
8935
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).'
|
|
8936
|
-
).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) => {
|
|
8937
8949
|
try {
|
|
8938
8950
|
const metadata = await buildMetadata(opts);
|
|
8951
|
+
const format = await resolveOutput(command, opts);
|
|
8939
8952
|
if (!opts.file) throw new Error("--file is required (multipart upload)");
|
|
8940
8953
|
const file = await readFile8(opts.file);
|
|
8941
|
-
const r = await new SdkDictionariesService().replace(id, {
|
|
8954
|
+
const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
|
|
8942
8955
|
metadata,
|
|
8943
8956
|
file,
|
|
8944
8957
|
includeKeywords: opts.includeKeywords
|
|
@@ -8946,22 +8959,23 @@ function register(dlp) {
|
|
|
8946
8959
|
if ("kind" in r && r.kind === "fallback") {
|
|
8947
8960
|
dlpDictionaries.renderReplaced204Fallback(id);
|
|
8948
8961
|
} else {
|
|
8949
|
-
dlpDictionaries.renderReplaced(r,
|
|
8962
|
+
dlpDictionaries.renderReplaced(r, format);
|
|
8950
8963
|
}
|
|
8951
8964
|
} catch (err) {
|
|
8952
8965
|
usageError(err instanceof Error ? err.message : String(err));
|
|
8953
8966
|
}
|
|
8954
8967
|
});
|
|
8955
|
-
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) => {
|
|
8956
8969
|
try {
|
|
8970
|
+
const format = await resolveOutput(command, opts);
|
|
8957
8971
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
8958
8972
|
throw new Error("--body-file is mutually exclusive with --set/--clear");
|
|
8959
8973
|
}
|
|
8960
8974
|
const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
8961
8975
|
dlpDictionaries.renderPatched(
|
|
8962
8976
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
8963
|
-
await new SdkDictionariesService().patch(id, body),
|
|
8964
|
-
|
|
8977
|
+
await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
|
|
8978
|
+
format
|
|
8965
8979
|
);
|
|
8966
8980
|
} catch (err) {
|
|
8967
8981
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -8969,7 +8983,7 @@ function register(dlp) {
|
|
|
8969
8983
|
});
|
|
8970
8984
|
group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
|
|
8971
8985
|
try {
|
|
8972
|
-
await new SdkDictionariesService().delete(id);
|
|
8986
|
+
await new SdkDictionariesService(await loadDlpClientOptions()).delete(id);
|
|
8973
8987
|
dlpDictionaries.renderDeleted(id);
|
|
8974
8988
|
} catch (err) {
|
|
8975
8989
|
fail(err);
|
|
@@ -9142,7 +9156,7 @@ function register2(dlp) {
|
|
|
9142
9156
|
listCmd.action(async (opts) => {
|
|
9143
9157
|
try {
|
|
9144
9158
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9145
|
-
const svc = new SdkDataFilteringProfilesService();
|
|
9159
|
+
const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
|
|
9146
9160
|
const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
|
|
9147
9161
|
const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort });
|
|
9148
9162
|
dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
|
|
@@ -9152,20 +9166,21 @@ function register2(dlp) {
|
|
|
9152
9166
|
});
|
|
9153
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) => {
|
|
9154
9168
|
try {
|
|
9155
|
-
const svc = new SdkDataFilteringProfilesService();
|
|
9169
|
+
const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
|
|
9156
9170
|
dlpFilteringProfiles.renderGet(await svc.get(id), await resolveOutput(getCmd, opts));
|
|
9157
9171
|
} catch (err) {
|
|
9158
9172
|
fail(err);
|
|
9159
9173
|
}
|
|
9160
9174
|
});
|
|
9161
|
-
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) => {
|
|
9162
9176
|
try {
|
|
9163
9177
|
const body = await resolveReplaceBody(opts);
|
|
9164
|
-
const
|
|
9178
|
+
const format = await resolveOutput(command, opts);
|
|
9179
|
+
const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
|
|
9165
9180
|
dlpFilteringProfiles.renderReplaced(
|
|
9166
9181
|
// biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
|
|
9167
9182
|
await svc.replace(id, body),
|
|
9168
|
-
|
|
9183
|
+
format
|
|
9169
9184
|
);
|
|
9170
9185
|
} catch (err) {
|
|
9171
9186
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -9310,7 +9325,7 @@ function register4(dlp) {
|
|
|
9310
9325
|
listCmd.action(async (opts) => {
|
|
9311
9326
|
try {
|
|
9312
9327
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9313
|
-
const svc = new SdkDataPatternsService();
|
|
9328
|
+
const svc = new SdkDataPatternsService(await loadDlpClientOptions());
|
|
9314
9329
|
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
|
|
9315
9330
|
dlpPatterns.renderList(
|
|
9316
9331
|
result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
|
|
@@ -9320,22 +9335,25 @@ function register4(dlp) {
|
|
|
9320
9335
|
fail(err);
|
|
9321
9336
|
}
|
|
9322
9337
|
});
|
|
9323
|
-
writeFlags(group.command("create").description("Create a data pattern")).action(
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
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
|
+
}
|
|
9333
9351
|
}
|
|
9334
|
-
|
|
9352
|
+
);
|
|
9335
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) => {
|
|
9336
9354
|
try {
|
|
9337
9355
|
dlpPatterns.renderGet(
|
|
9338
|
-
await new SdkDataPatternsService().get(id),
|
|
9356
|
+
await new SdkDataPatternsService(await loadDlpClientOptions()).get(id),
|
|
9339
9357
|
await resolveOutput(getCmd, opts)
|
|
9340
9358
|
);
|
|
9341
9359
|
} catch (err) {
|
|
@@ -9343,13 +9361,14 @@ function register4(dlp) {
|
|
|
9343
9361
|
}
|
|
9344
9362
|
});
|
|
9345
9363
|
writeFlags(group.command("replace <id>").description("Full-replace a data pattern (PUT)")).action(
|
|
9346
|
-
async (id, opts) => {
|
|
9364
|
+
async (id, opts, command) => {
|
|
9347
9365
|
try {
|
|
9366
|
+
const format = await resolveOutput(command, opts);
|
|
9348
9367
|
const body = await resolveWriteBody(opts);
|
|
9349
9368
|
dlpPatterns.renderReplaced(
|
|
9350
9369
|
// biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
|
|
9351
|
-
await new SdkDataPatternsService().replace(id, body),
|
|
9352
|
-
|
|
9370
|
+
await new SdkDataPatternsService(await loadDlpClientOptions()).replace(id, body),
|
|
9371
|
+
format
|
|
9353
9372
|
);
|
|
9354
9373
|
} catch (err) {
|
|
9355
9374
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -9358,16 +9377,17 @@ function register4(dlp) {
|
|
|
9358
9377
|
);
|
|
9359
9378
|
group.command("patch <id>").description(
|
|
9360
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"'.`
|
|
9361
|
-
).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) => {
|
|
9362
9381
|
try {
|
|
9382
|
+
const format = await resolveOutput(command, opts);
|
|
9363
9383
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
9364
9384
|
throw new Error("--body-file is mutually exclusive with --set/--clear");
|
|
9365
9385
|
}
|
|
9366
9386
|
const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
9367
9387
|
dlpPatterns.renderPatched(
|
|
9368
9388
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
9369
|
-
await new SdkDataPatternsService().patch(id, body),
|
|
9370
|
-
|
|
9389
|
+
await new SdkDataPatternsService(await loadDlpClientOptions()).patch(id, body),
|
|
9390
|
+
format
|
|
9371
9391
|
);
|
|
9372
9392
|
} catch (err) {
|
|
9373
9393
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -9375,7 +9395,7 @@ function register4(dlp) {
|
|
|
9375
9395
|
});
|
|
9376
9396
|
group.command("delete <id>").description("Soft-delete (archive) a data pattern").action(async (id) => {
|
|
9377
9397
|
try {
|
|
9378
|
-
await new SdkDataPatternsService().delete(id);
|
|
9398
|
+
await new SdkDataPatternsService(await loadDlpClientOptions()).delete(id);
|
|
9379
9399
|
dlpPatterns.renderArchived(id);
|
|
9380
9400
|
} catch (err) {
|
|
9381
9401
|
fail(err);
|
|
@@ -9451,7 +9471,7 @@ function register5(dlp) {
|
|
|
9451
9471
|
listCmd.action(async (opts) => {
|
|
9452
9472
|
try {
|
|
9453
9473
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9454
|
-
const svc = new SdkDataProfilesService();
|
|
9474
|
+
const svc = new SdkDataProfilesService(await loadDlpClientOptions());
|
|
9455
9475
|
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
|
|
9456
9476
|
dlpProfiles.renderList(
|
|
9457
9477
|
result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
|
|
@@ -9461,22 +9481,25 @@ function register5(dlp) {
|
|
|
9461
9481
|
fail(err);
|
|
9462
9482
|
}
|
|
9463
9483
|
});
|
|
9464
|
-
writeFlags2(group.command("create").description("Create a data profile")).action(
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
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
|
+
}
|
|
9474
9497
|
}
|
|
9475
|
-
|
|
9498
|
+
);
|
|
9476
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) => {
|
|
9477
9500
|
try {
|
|
9478
9501
|
dlpProfiles.renderGet(
|
|
9479
|
-
await new SdkDataProfilesService().get(id),
|
|
9502
|
+
await new SdkDataProfilesService(await loadDlpClientOptions()).get(id),
|
|
9480
9503
|
await resolveOutput(getCmd, opts)
|
|
9481
9504
|
);
|
|
9482
9505
|
} catch (err) {
|
|
@@ -9484,13 +9507,14 @@ function register5(dlp) {
|
|
|
9484
9507
|
}
|
|
9485
9508
|
});
|
|
9486
9509
|
writeFlags2(group.command("replace <id>").description("Full-replace a data profile (PUT)")).action(
|
|
9487
|
-
async (id, opts) => {
|
|
9510
|
+
async (id, opts, command) => {
|
|
9488
9511
|
try {
|
|
9512
|
+
const format = await resolveOutput(command, opts);
|
|
9489
9513
|
const body = await resolveWriteBody2(opts);
|
|
9490
9514
|
dlpProfiles.renderReplaced(
|
|
9491
9515
|
// biome-ignore lint/suspicious/noExplicitAny: body shape verified by SDK Zod
|
|
9492
|
-
await new SdkDataProfilesService().replace(id, body),
|
|
9493
|
-
|
|
9516
|
+
await new SdkDataProfilesService(await loadDlpClientOptions()).replace(id, body),
|
|
9517
|
+
format
|
|
9494
9518
|
);
|
|
9495
9519
|
} catch (err) {
|
|
9496
9520
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -9499,16 +9523,17 @@ function register5(dlp) {
|
|
|
9499
9523
|
);
|
|
9500
9524
|
group.command("patch <id>").description(
|
|
9501
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"'.`
|
|
9502
|
-
).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) => {
|
|
9503
9527
|
try {
|
|
9528
|
+
const format = await resolveOutput(command, opts);
|
|
9504
9529
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
9505
9530
|
throw new Error("--body-file is mutually exclusive with --set/--clear");
|
|
9506
9531
|
}
|
|
9507
9532
|
const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
9508
9533
|
dlpProfiles.renderPatched(
|
|
9509
9534
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
9510
|
-
await new SdkDataProfilesService().patch(id, body),
|
|
9511
|
-
|
|
9535
|
+
await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
|
|
9536
|
+
format
|
|
9512
9537
|
);
|
|
9513
9538
|
} catch (err) {
|
|
9514
9539
|
usageError(err instanceof Error ? err.message : String(err));
|
|
@@ -9551,6 +9576,102 @@ import {
|
|
|
9551
9576
|
SecurityProfileSchema
|
|
9552
9577
|
} from "@cdot65/prisma-airs-sdk";
|
|
9553
9578
|
import { z as z5 } from "zod";
|
|
9579
|
+
|
|
9580
|
+
// src/backup/runtime-policy.ts
|
|
9581
|
+
function object(value) {
|
|
9582
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9583
|
+
}
|
|
9584
|
+
function compareRuntimePolicies(expected, actual) {
|
|
9585
|
+
const serverDefaults = [];
|
|
9586
|
+
const differences = [];
|
|
9587
|
+
const visit = (source, target, path3, insideToxicCategories = false, insideTopicGuardrails = false) => {
|
|
9588
|
+
if (source === target) return;
|
|
9589
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
9590
|
+
if (source.length !== target.length) differences.push(`${path3}.length`);
|
|
9591
|
+
source.forEach((item, index) => {
|
|
9592
|
+
visit(
|
|
9593
|
+
item,
|
|
9594
|
+
target[index],
|
|
9595
|
+
`${path3}[${index}]`,
|
|
9596
|
+
insideToxicCategories,
|
|
9597
|
+
insideTopicGuardrails
|
|
9598
|
+
);
|
|
9599
|
+
});
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
if (object(source) && object(target)) {
|
|
9603
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
|
|
9604
|
+
const field = `${path3}.${key}`;
|
|
9605
|
+
if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
|
|
9606
|
+
const model = /^policy\.ai-security-profiles\[\d+\]\.model-configuration/;
|
|
9607
|
+
const relative = path3.replace(model, "model");
|
|
9608
|
+
let allowed;
|
|
9609
|
+
if (relative === "model.data-protection" && key === "database-security") allowed = null;
|
|
9610
|
+
if (key === "severity") {
|
|
9611
|
+
if (/^model\.data-protection\.database-security\[\d+\]$/.test(relative)) {
|
|
9612
|
+
const values = {
|
|
9613
|
+
"database-security-create": "medium",
|
|
9614
|
+
"database-security-read": "low",
|
|
9615
|
+
"database-security-update": "medium",
|
|
9616
|
+
"database-security-delete": "high"
|
|
9617
|
+
};
|
|
9618
|
+
if (source.name === target.name && typeof source.name === "string")
|
|
9619
|
+
allowed = Object.hasOwn(values, source.name) ? values[source.name] : void 0;
|
|
9620
|
+
}
|
|
9621
|
+
if (relative === "model.data-protection.source-code-detection") allowed = "high";
|
|
9622
|
+
if (relative === "model.app-protection.malicious-code-protection" && source.name === "malicious-code" && target.name === source.name)
|
|
9623
|
+
allowed = "high";
|
|
9624
|
+
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)
|
|
9625
|
+
allowed = "medium";
|
|
9626
|
+
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)
|
|
9627
|
+
allowed = "medium";
|
|
9628
|
+
if (/^model\.agent-protection\[\d+\]$/.test(relative) && source.name === "agent-security" && target.name === source.name)
|
|
9629
|
+
allowed = "medium";
|
|
9630
|
+
}
|
|
9631
|
+
if (relative === "model.app-protection" && key === "url-detected-severity")
|
|
9632
|
+
allowed = "low";
|
|
9633
|
+
if (relative === "model.app-protection" && key === "default-url-category" && object(target[key])) {
|
|
9634
|
+
const value = target[key];
|
|
9635
|
+
if (Object.keys(value).length === 1 && value.member === null) {
|
|
9636
|
+
serverDefaults.push(field);
|
|
9637
|
+
continue;
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
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])) {
|
|
9641
|
+
const value = target[key];
|
|
9642
|
+
if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
|
|
9643
|
+
serverDefaults.push(field);
|
|
9644
|
+
continue;
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
if (allowed !== void 0 && target[key] === allowed) {
|
|
9648
|
+
serverDefaults.push(field);
|
|
9649
|
+
continue;
|
|
9650
|
+
}
|
|
9651
|
+
differences.push(field);
|
|
9652
|
+
} else if (!Object.hasOwn(target, key)) differences.push(field);
|
|
9653
|
+
else
|
|
9654
|
+
visit(
|
|
9655
|
+
source[key],
|
|
9656
|
+
target[key],
|
|
9657
|
+
field,
|
|
9658
|
+
key === "toxic-category-list" && source.name === "toxic-content" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
|
|
9659
|
+
path3
|
|
9660
|
+
),
|
|
9661
|
+
key === "topic-list" && source.name === "topic-guardrails" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
|
|
9662
|
+
path3
|
|
9663
|
+
) || insideTopicGuardrails && key === "topic" && source.action === "block" && target.action === "block"
|
|
9664
|
+
);
|
|
9665
|
+
}
|
|
9666
|
+
return;
|
|
9667
|
+
}
|
|
9668
|
+
differences.push(path3);
|
|
9669
|
+
};
|
|
9670
|
+
visit(expected, actual, "policy");
|
|
9671
|
+
return { matches: differences.length === 0, serverDefaults, differences };
|
|
9672
|
+
}
|
|
9673
|
+
|
|
9674
|
+
// src/backup/runtime-profiles.ts
|
|
9554
9675
|
var RuntimeProfilesBackupSchema = z5.object({
|
|
9555
9676
|
version: z5.literal(1),
|
|
9556
9677
|
resourceType: z5.literal("runtime-security-profiles"),
|
|
@@ -9559,7 +9680,7 @@ var RuntimeProfilesBackupSchema = z5.object({
|
|
|
9559
9680
|
profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
|
|
9560
9681
|
topics: z5.array(CustomTopicSchema).max(1e4)
|
|
9561
9682
|
}).strict();
|
|
9562
|
-
function
|
|
9683
|
+
function object2(value) {
|
|
9563
9684
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9564
9685
|
}
|
|
9565
9686
|
function pagesLimit(value) {
|
|
@@ -9626,7 +9747,7 @@ function topicReferences(policy) {
|
|
|
9626
9747
|
value.forEach(visit);
|
|
9627
9748
|
return;
|
|
9628
9749
|
}
|
|
9629
|
-
if (!
|
|
9750
|
+
if (!object2(value)) return;
|
|
9630
9751
|
if ("topic_id" in value) {
|
|
9631
9752
|
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9632
9753
|
throw new Error("Invalid topic identity in profile policy");
|
|
@@ -9679,26 +9800,48 @@ function sameTopic(a, b) {
|
|
|
9679
9800
|
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9680
9801
|
}
|
|
9681
9802
|
function dlpReferences(policy) {
|
|
9682
|
-
if (!
|
|
9803
|
+
if (!object2(policy)) return [];
|
|
9683
9804
|
const refs = [];
|
|
9684
9805
|
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9685
|
-
if (!
|
|
9806
|
+
if (!object2(entry) || typeof entry.name !== "string")
|
|
9686
9807
|
throw new Error("Invalid embedded DLP reference");
|
|
9687
9808
|
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9688
9809
|
}
|
|
9689
9810
|
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9690
|
-
if (!
|
|
9811
|
+
if (!object2(entry) || !object2(entry["model-configuration"])) continue;
|
|
9691
9812
|
const protection = entry["model-configuration"]["data-protection"];
|
|
9692
|
-
if (!
|
|
9813
|
+
if (!object2(protection) || !object2(protection["data-leak-detection"])) continue;
|
|
9693
9814
|
const members = protection["data-leak-detection"].member;
|
|
9694
9815
|
if (!Array.isArray(members)) continue;
|
|
9695
9816
|
for (const member of members) {
|
|
9696
|
-
if (!
|
|
9817
|
+
if (!object2(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9818
|
+
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))
|
|
9819
|
+
continue;
|
|
9697
9820
|
refs.push({ value: member, name: member.text, kind: "member" });
|
|
9698
9821
|
}
|
|
9699
9822
|
}
|
|
9700
9823
|
return refs;
|
|
9701
9824
|
}
|
|
9825
|
+
function basicFallback(policy) {
|
|
9826
|
+
const result = structuredClone(policy);
|
|
9827
|
+
const customNames = new Set(dlpReferences(result).map((ref) => ref.name));
|
|
9828
|
+
let converted = false;
|
|
9829
|
+
for (const entry of result?.["ai-security-profiles"] ?? []) {
|
|
9830
|
+
const detection = entry["model-configuration"]?.["data-protection"]?.["data-leak-detection"];
|
|
9831
|
+
if (!detection?.member?.some((member) => customNames.has(member.text))) continue;
|
|
9832
|
+
if (!["block", "allow", ""].includes(detection.action))
|
|
9833
|
+
throw new Error("Cannot safely fall back to Basic: unsupported DLP action");
|
|
9834
|
+
if (detection["mask-data-inline"] && detection.action !== "block")
|
|
9835
|
+
throw new Error("Cannot safely fall back to Basic: inline masking requires a block action");
|
|
9836
|
+
detection.member = detection.action === "" ? null : [{ text: "sensitive content", id: "", version: "2" }];
|
|
9837
|
+
converted = true;
|
|
9838
|
+
}
|
|
9839
|
+
if (!converted)
|
|
9840
|
+
throw new Error("Cannot safely fall back to Basic: no matching DLP detection configuration");
|
|
9841
|
+
if (result && "dlp-data-profiles" in result) result["dlp-data-profiles"] = [];
|
|
9842
|
+
PolicySchema.parse(result);
|
|
9843
|
+
return result;
|
|
9844
|
+
}
|
|
9702
9845
|
function assertKnownPolicyReferences(policy) {
|
|
9703
9846
|
const known = new Map([
|
|
9704
9847
|
...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
|
|
@@ -9714,7 +9857,7 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9714
9857
|
});
|
|
9715
9858
|
return;
|
|
9716
9859
|
}
|
|
9717
|
-
if (!
|
|
9860
|
+
if (!object2(value)) return;
|
|
9718
9861
|
for (const [key, child] of Object.entries(value)) {
|
|
9719
9862
|
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9720
9863
|
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
@@ -9744,7 +9887,10 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9744
9887
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9745
9888
|
throw new Error("Invalid --name-prefix");
|
|
9746
9889
|
const conflict = options.onConflict ?? "error";
|
|
9747
|
-
if (!["error", "skip", "update"].includes(conflict))
|
|
9890
|
+
if (!["error", "skip", "update", "verify"].includes(conflict))
|
|
9891
|
+
throw new Error("Invalid conflict policy");
|
|
9892
|
+
const missingDlp = options.onMissingDlp ?? "error";
|
|
9893
|
+
if (!["error", "basic"].includes(missingDlp)) throw new Error("Invalid missing DLP policy");
|
|
9748
9894
|
const selected = latestProfiles(backup.profiles);
|
|
9749
9895
|
assertTenant(selected, backup.source.tsgId);
|
|
9750
9896
|
if (selected.length !== backup.profiles.length)
|
|
@@ -9766,14 +9912,14 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9766
9912
|
const match = existing.find((p) => p.profile_name === name);
|
|
9767
9913
|
if (match && conflict === "error")
|
|
9768
9914
|
throw new Error(
|
|
9769
|
-
`Destination profile already exists: ${name}; choose --on-conflict skip or update`
|
|
9915
|
+
`Destination profile already exists: ${name}; choose --on-conflict verify, skip or update`
|
|
9770
9916
|
);
|
|
9771
9917
|
if (match && !match.profile_id) throw new Error("Destination profile has no identity");
|
|
9772
9918
|
return {
|
|
9773
9919
|
source: structuredClone(source),
|
|
9774
9920
|
name,
|
|
9775
9921
|
existing: match,
|
|
9776
|
-
action: match ? conflict === "skip" ? "skip" : "update" : "create"
|
|
9922
|
+
action: match ? conflict === "skip" ? "skip" : conflict === "verify" ? "verify" : "update" : "create"
|
|
9777
9923
|
};
|
|
9778
9924
|
});
|
|
9779
9925
|
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
@@ -9807,46 +9953,102 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9807
9953
|
);
|
|
9808
9954
|
topicPlans.set(key, { key, name, source, existing: match });
|
|
9809
9955
|
}
|
|
9810
|
-
const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9956
|
+
const dlpRefs = crossTenant || missingDlp === "basic" ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9811
9957
|
const names = [...new Set(dlpRefs.map((ref) => ref.name))];
|
|
9812
|
-
|
|
9813
|
-
|
|
9958
|
+
const targetFor = (name) => Object.hasOwn(options.dlpMap ?? {}, name) ? options.dlpMap?.[name] : crossTenant ? void 0 : name;
|
|
9959
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
9960
|
+
for (const name of names) {
|
|
9961
|
+
if (!targetFor(name) && missingDlp === "error")
|
|
9814
9962
|
throw new Error(
|
|
9815
|
-
`Cross-tenant DLP reference requires explicit --dlp-map ${name}=<destination-name
|
|
9963
|
+
`Cross-tenant DLP reference requires explicit --dlp-map "${name}=<destination-name>"; DLP resources are not cloned. Inspect destination resources with airs runtime dlp profiles list --all --output json, or explicitly accept --on-missing-dlp basic.`
|
|
9816
9964
|
);
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9965
|
+
if (!targetFor(name)) unresolved.add(name);
|
|
9966
|
+
}
|
|
9967
|
+
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages2) : [];
|
|
9968
|
+
const resolved = [];
|
|
9969
|
+
for (const name of names) {
|
|
9970
|
+
const target = targetFor(name);
|
|
9971
|
+
if (!target) continue;
|
|
9972
|
+
const matches = catalog.filter(
|
|
9973
|
+
(p) => p.name === target && p.profile_status !== "deleted" && p.profile_status !== "disabled"
|
|
9974
|
+
);
|
|
9975
|
+
if (!matches.length && missingDlp === "basic") {
|
|
9976
|
+
unresolved.add(name);
|
|
9977
|
+
continue;
|
|
9978
|
+
}
|
|
9821
9979
|
if (matches.length !== 1 || !matches[0].id)
|
|
9822
9980
|
throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
|
|
9823
9981
|
if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
|
|
9824
9982
|
throw new Error(`Destination DLP version is unknown: ${target}`);
|
|
9825
|
-
|
|
9983
|
+
resolved.push({
|
|
9826
9984
|
source: name,
|
|
9827
9985
|
destination: String(target),
|
|
9828
9986
|
id: matches[0].id,
|
|
9829
9987
|
version: matches[0].version ?? void 0
|
|
9830
|
-
};
|
|
9831
|
-
}
|
|
9832
|
-
|
|
9988
|
+
});
|
|
9989
|
+
}
|
|
9990
|
+
const dlpFallbacks = [];
|
|
9991
|
+
for (const item of changing) {
|
|
9992
|
+
const replaced = [...new Set(dlpReferences(item.source.policy).map((ref) => ref.name))];
|
|
9993
|
+
const missing = replaced.filter((name) => unresolved.has(name));
|
|
9994
|
+
if (!missing.length) continue;
|
|
9995
|
+
item.fallbackPolicy = basicFallback(item.source.policy);
|
|
9996
|
+
dlpFallbacks.push({
|
|
9997
|
+
profile: item.name,
|
|
9998
|
+
unresolved: missing,
|
|
9999
|
+
replaced,
|
|
10000
|
+
reason: "unmapped-or-missing",
|
|
10001
|
+
protection: "basic"
|
|
10002
|
+
});
|
|
10003
|
+
}
|
|
10004
|
+
const remaining = new Set(
|
|
10005
|
+
changing.flatMap(
|
|
10006
|
+
(p) => dlpReferences(p.fallbackPolicy ?? p.source.policy).map((ref) => ref.name)
|
|
10007
|
+
)
|
|
10008
|
+
);
|
|
10009
|
+
const dlpMappings = resolved.filter((mapping) => remaining.has(mapping.source));
|
|
10010
|
+
const plan = {
|
|
9833
10011
|
sourceTsgId: backup.source.tsgId,
|
|
9834
10012
|
destinationTsgId,
|
|
9835
10013
|
profiles: profilePlans,
|
|
9836
10014
|
topics: [...topicPlans.values()],
|
|
9837
10015
|
dlpMappings,
|
|
10016
|
+
dlpFallbacks,
|
|
10017
|
+
serverDefaults: [],
|
|
9838
10018
|
maxPages: maxPages2
|
|
9839
10019
|
};
|
|
10020
|
+
const existingTopics = new Map(
|
|
10021
|
+
plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
|
|
10022
|
+
);
|
|
10023
|
+
for (const item of profilePlans.filter((p) => p.action === "verify")) {
|
|
10024
|
+
const expected = mappedPolicy(item, plan, existingTopics);
|
|
10025
|
+
const comparison = compareRuntimePolicies(expected, item.existing?.policy);
|
|
10026
|
+
if (!comparison.matches || item.source.active !== void 0 && item.existing?.active !== item.source.active)
|
|
10027
|
+
throw new Error(
|
|
10028
|
+
`Destination profile does not match restore plan: ${item.name}; verification made no changes`
|
|
10029
|
+
);
|
|
10030
|
+
if (comparison.serverDefaults.length)
|
|
10031
|
+
plan.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
|
|
10032
|
+
}
|
|
10033
|
+
return plan;
|
|
9840
10034
|
}
|
|
9841
10035
|
function canonical(value) {
|
|
9842
|
-
const order = (v) => Array.isArray(v) ? v.map(order) :
|
|
10036
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object2(v) ? Object.fromEntries(
|
|
9843
10037
|
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9844
10038
|
) : v;
|
|
9845
10039
|
return JSON.stringify(order(value));
|
|
9846
10040
|
}
|
|
10041
|
+
function canonicalPolicy(policy) {
|
|
10042
|
+
const value = structuredClone(policy);
|
|
10043
|
+
for (const entry of value?.["ai-security-profiles"] ?? []) {
|
|
10044
|
+
const protection = entry["model-configuration"]?.["data-protection"];
|
|
10045
|
+
if (protection && !("database-security" in protection)) protection["database-security"] = null;
|
|
10046
|
+
}
|
|
10047
|
+
return canonical(value);
|
|
10048
|
+
}
|
|
9847
10049
|
function unchanged(current, prior) {
|
|
9848
10050
|
return Boolean(
|
|
9849
|
-
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active &&
|
|
10051
|
+
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonicalPolicy(current.policy) === canonicalPolicy(prior.policy)
|
|
9850
10052
|
);
|
|
9851
10053
|
}
|
|
9852
10054
|
async function restoreRuntimeProfiles(api, plan) {
|
|
@@ -9854,6 +10056,8 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9854
10056
|
sourceTsgId: plan.sourceTsgId,
|
|
9855
10057
|
destinationTsgId: plan.destinationTsgId,
|
|
9856
10058
|
complete: false,
|
|
10059
|
+
dlpFallbacks: structuredClone(plan.dlpFallbacks ?? []),
|
|
10060
|
+
serverDefaults: [],
|
|
9857
10061
|
topics: [],
|
|
9858
10062
|
profiles: []
|
|
9859
10063
|
};
|
|
@@ -9864,7 +10068,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9864
10068
|
const catalog = await dataProfiles(api, plan.maxPages);
|
|
9865
10069
|
for (const mapping of plan.dlpMappings) {
|
|
9866
10070
|
const matches = catalog.filter(
|
|
9867
|
-
(p) => p.name === mapping.destination && p.profile_status !== "deleted"
|
|
10071
|
+
(p) => p.name === mapping.destination && p.profile_status !== "deleted" && p.profile_status !== "disabled"
|
|
9868
10072
|
);
|
|
9869
10073
|
if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
|
|
9870
10074
|
throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
|
|
@@ -9910,27 +10114,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9910
10114
|
result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
|
|
9911
10115
|
continue;
|
|
9912
10116
|
}
|
|
9913
|
-
const policy =
|
|
9914
|
-
for (const ref of topicReferences(policy)) {
|
|
9915
|
-
const topic = mappedTopics.get(topicKey(ref));
|
|
9916
|
-
if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
|
|
9917
|
-
ref.topic_id = topic.topic_id;
|
|
9918
|
-
ref.topic_name = topic.topic_name;
|
|
9919
|
-
ref.revision = topic.revision;
|
|
9920
|
-
}
|
|
9921
|
-
for (const ref of dlpReferences(policy)) {
|
|
9922
|
-
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
9923
|
-
if (!mapping) continue;
|
|
9924
|
-
if (ref.kind === "embedded") {
|
|
9925
|
-
ref.value.name = mapping.destination;
|
|
9926
|
-
ref.value.uuid = mapping.id;
|
|
9927
|
-
} else ref.value.text = mapping.destination;
|
|
9928
|
-
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
9929
|
-
if ("version" in ref.value) {
|
|
9930
|
-
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
9931
|
-
ref.value.version = String(mapping.version);
|
|
9932
|
-
}
|
|
9933
|
-
}
|
|
10117
|
+
const policy = mappedPolicy(item, plan, mappedTopics);
|
|
9934
10118
|
const body = CreateSecurityProfileRequestSchema.parse({
|
|
9935
10119
|
profile_name: item.name,
|
|
9936
10120
|
active: item.source.active,
|
|
@@ -9941,6 +10125,14 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9941
10125
|
const current = inventory2.find((p) => p.profile_name === item.name);
|
|
9942
10126
|
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9943
10127
|
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
10128
|
+
if (item.action === "verify") {
|
|
10129
|
+
const comparison2 = compareRuntimePolicies(policy, current?.policy);
|
|
10130
|
+
if (!comparison2.matches) throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
10131
|
+
result.profiles.push({ name: item.name, action: "verified", id: current?.profile_id });
|
|
10132
|
+
if (comparison2.serverDefaults.length)
|
|
10133
|
+
result.serverDefaults.push({ profile: item.name, fields: comparison2.serverDefaults });
|
|
10134
|
+
continue;
|
|
10135
|
+
}
|
|
9944
10136
|
const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
|
|
9945
10137
|
result.profiles.push({
|
|
9946
10138
|
name: item.name,
|
|
@@ -9951,8 +10143,11 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9951
10143
|
(p) => p.profile_name === item.name
|
|
9952
10144
|
);
|
|
9953
10145
|
if (verified) assertTenant([verified], plan.destinationTsgId);
|
|
9954
|
-
|
|
10146
|
+
const comparison = compareRuntimePolicies(policy, verified?.policy);
|
|
10147
|
+
if (!verified?.profile_id || response.profile_id && response.profile_id !== verified.profile_id || item.source.active !== void 0 && verified.active !== item.source.active || !comparison.matches)
|
|
9955
10148
|
throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
10149
|
+
if (comparison.serverDefaults.length)
|
|
10150
|
+
result.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
|
|
9956
10151
|
}
|
|
9957
10152
|
result.complete = true;
|
|
9958
10153
|
} catch (error) {
|
|
@@ -9963,10 +10158,97 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9963
10158
|
}
|
|
9964
10159
|
return result;
|
|
9965
10160
|
}
|
|
10161
|
+
function mappedPolicy(item, plan, mappedTopics) {
|
|
10162
|
+
const policy = structuredClone(item.fallbackPolicy ?? item.source.policy);
|
|
10163
|
+
for (const ref of topicReferences(policy)) {
|
|
10164
|
+
const topic = mappedTopics.get(topicKey(ref));
|
|
10165
|
+
if (!topic?.topic_id)
|
|
10166
|
+
throw new Error(
|
|
10167
|
+
"Missing planned topic mapping; an existing profile cannot be verified without its destination dependencies"
|
|
10168
|
+
);
|
|
10169
|
+
ref.topic_id = topic.topic_id;
|
|
10170
|
+
ref.topic_name = topic.topic_name;
|
|
10171
|
+
ref.revision = topic.revision;
|
|
10172
|
+
}
|
|
10173
|
+
for (const ref of dlpReferences(policy)) {
|
|
10174
|
+
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
10175
|
+
if (!mapping) continue;
|
|
10176
|
+
if (ref.kind === "embedded") {
|
|
10177
|
+
ref.value.name = mapping.destination;
|
|
10178
|
+
ref.value.uuid = mapping.id;
|
|
10179
|
+
} else ref.value.text = mapping.destination;
|
|
10180
|
+
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
10181
|
+
if ("version" in ref.value) {
|
|
10182
|
+
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
10183
|
+
ref.value.version = String(mapping.version);
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
return policy;
|
|
10187
|
+
}
|
|
9966
10188
|
|
|
9967
10189
|
// src/cli/commands/profile-transfer.ts
|
|
9968
10190
|
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
9969
10191
|
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
10192
|
+
function renderRestore(value, columns, format) {
|
|
10193
|
+
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10194
|
+
return formatOutput([value], columns, format);
|
|
10195
|
+
const markdown = format === "markdown";
|
|
10196
|
+
const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
|
|
10197
|
+
const heading = (text2) => markdown ? `### ${text2}
|
|
10198
|
+
` : text2;
|
|
10199
|
+
const blocks = [
|
|
10200
|
+
heading(
|
|
10201
|
+
value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
|
|
10202
|
+
),
|
|
10203
|
+
`Source TSG: ${value.sourceTsgId}
|
|
10204
|
+
${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
10205
|
+
];
|
|
10206
|
+
for (const [key, title] of [
|
|
10207
|
+
["profiles", "Profiles"],
|
|
10208
|
+
["topics", "Topics"]
|
|
10209
|
+
]) {
|
|
10210
|
+
blocks.push(heading(`${title} (${rows(key).length})`));
|
|
10211
|
+
blocks.push(
|
|
10212
|
+
formatOutput(
|
|
10213
|
+
rows(key),
|
|
10214
|
+
[
|
|
10215
|
+
{ key: "action", label: "Action" },
|
|
10216
|
+
{ key: "name", label: "Name" }
|
|
10217
|
+
],
|
|
10218
|
+
markdown ? "markdown" : "table"
|
|
10219
|
+
) || "(none)"
|
|
10220
|
+
);
|
|
10221
|
+
}
|
|
10222
|
+
if (rows("dlpMappings").length) {
|
|
10223
|
+
blocks.push(heading("DLP mappings"));
|
|
10224
|
+
blocks.push(
|
|
10225
|
+
formatOutput(
|
|
10226
|
+
rows("dlpMappings"),
|
|
10227
|
+
[
|
|
10228
|
+
{ key: "source", label: "Source" },
|
|
10229
|
+
{ key: "destination", label: "Destination" }
|
|
10230
|
+
],
|
|
10231
|
+
markdown ? "markdown" : "table"
|
|
10232
|
+
)
|
|
10233
|
+
);
|
|
10234
|
+
}
|
|
10235
|
+
if (rows("dlpFallbacks").length) {
|
|
10236
|
+
blocks.push(heading("Basic DLP fallbacks \u2014 custom protection lost"));
|
|
10237
|
+
for (const fallback of rows("dlpFallbacks"))
|
|
10238
|
+
blocks.push(
|
|
10239
|
+
`${fallback.profile}: ${Array.isArray(fallback.replaced) ? fallback.replaced.join(", ") : ""}`
|
|
10240
|
+
);
|
|
10241
|
+
}
|
|
10242
|
+
if (rows("serverDefaults").length) {
|
|
10243
|
+
blocks.push(heading("Verified server-added defaults (source omitted these fields)"));
|
|
10244
|
+
for (const item of rows("serverDefaults"))
|
|
10245
|
+
blocks.push(
|
|
10246
|
+
`${item.profile}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
|
|
10247
|
+
);
|
|
10248
|
+
}
|
|
10249
|
+
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10250
|
+
return blocks.join("\n\n");
|
|
10251
|
+
}
|
|
9970
10252
|
function maxPages(value) {
|
|
9971
10253
|
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
9972
10254
|
usageError("--max-pages must be an integer from 1 to 1000");
|
|
@@ -10079,7 +10361,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10079
10361
|
});
|
|
10080
10362
|
const restore = profiles2.command("restore <file>").description(
|
|
10081
10363
|
"Restore profile policies into the selected tenant; validate all dependencies before changes"
|
|
10082
|
-
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option(
|
|
10364
|
+
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option(
|
|
10365
|
+
"--on-conflict <policy>",
|
|
10366
|
+
"Existing profile names: error, verify (resume without writes), skip or update",
|
|
10367
|
+
"error"
|
|
10368
|
+
).option(
|
|
10369
|
+
"--on-missing-dlp <policy>",
|
|
10370
|
+
"Unresolved custom DLP: error or basic (explicitly accepts loss of custom detection)",
|
|
10371
|
+
"error"
|
|
10372
|
+
).option(
|
|
10083
10373
|
"--dlp-map <source=destination>",
|
|
10084
10374
|
"Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
|
|
10085
10375
|
(value, prior) => [...prior, value],
|
|
@@ -10096,12 +10386,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10096
10386
|
"after",
|
|
10097
10387
|
examples(
|
|
10098
10388
|
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
10389
|
+
"airs runtime profiles restore ./profiles.json --on-missing-dlp basic --dry-run --output json",
|
|
10099
10390
|
"airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10100
10391
|
)
|
|
10101
10392
|
).action(async (file, opts) => {
|
|
10102
10393
|
try {
|
|
10103
|
-
if (!["error", "skip", "update"].includes(opts.onConflict))
|
|
10104
|
-
throw new CliUsageError("--on-conflict must be error, skip or update");
|
|
10394
|
+
if (!["error", "skip", "update", "verify"].includes(opts.onConflict))
|
|
10395
|
+
throw new CliUsageError("--on-conflict must be error, verify, skip or update");
|
|
10396
|
+
if (!["error", "basic"].includes(opts.onMissingDlp))
|
|
10397
|
+
throw new CliUsageError("--on-missing-dlp must be error or basic");
|
|
10105
10398
|
if (opts.force && !opts.expectTsg)
|
|
10106
10399
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10107
10400
|
const mappings = dlpMap(opts.dlpMap);
|
|
@@ -10131,7 +10424,8 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10131
10424
|
namePrefix: opts.namePrefix,
|
|
10132
10425
|
onConflict: opts.onConflict,
|
|
10133
10426
|
maxPages: opts.maxPages,
|
|
10134
|
-
dlpMap: mappings
|
|
10427
|
+
dlpMap: mappings,
|
|
10428
|
+
onMissingDlp: opts.onMissingDlp
|
|
10135
10429
|
});
|
|
10136
10430
|
const summary = {
|
|
10137
10431
|
sourceTsgId: plan.sourceTsgId,
|
|
@@ -10145,7 +10439,9 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10145
10439
|
dlpMappings: plan.dlpMappings.map((m) => ({
|
|
10146
10440
|
source: m.source,
|
|
10147
10441
|
destination: m.destination
|
|
10148
|
-
}))
|
|
10442
|
+
})),
|
|
10443
|
+
dlpFallbacks: plan.dlpFallbacks,
|
|
10444
|
+
serverDefaults: plan.serverDefaults
|
|
10149
10445
|
};
|
|
10150
10446
|
const columns = [
|
|
10151
10447
|
{ key: "sourceTsgId", label: "Source TSG" },
|
|
@@ -10153,22 +10449,30 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10153
10449
|
{ key: "profiles", label: "Profiles" },
|
|
10154
10450
|
{ key: "topics", label: "Topics" },
|
|
10155
10451
|
{ key: "dlpMappings", label: "DLP mappings" },
|
|
10452
|
+
{ key: "dlpFallbacks", label: "Basic DLP fallbacks (custom protection lost)" },
|
|
10453
|
+
{ key: "serverDefaults", label: "Verified server-added defaults" },
|
|
10156
10454
|
{ key: "dryRun", label: "Dry run" },
|
|
10157
10455
|
{ key: "complete", label: "Complete" }
|
|
10158
10456
|
];
|
|
10457
|
+
for (const fallback of plan.dlpFallbacks)
|
|
10458
|
+
ui.warning(
|
|
10459
|
+
`DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
|
|
10460
|
+
);
|
|
10159
10461
|
if (opts.dryRun) {
|
|
10160
|
-
console.log(
|
|
10462
|
+
console.log(renderRestore(summary, columns, format));
|
|
10161
10463
|
return;
|
|
10162
10464
|
}
|
|
10163
|
-
const changing = plan.profiles.filter(
|
|
10465
|
+
const changing = plan.profiles.filter(
|
|
10466
|
+
(p) => p.action === "create" || p.action === "update"
|
|
10467
|
+
);
|
|
10164
10468
|
if (changing.length)
|
|
10165
10469
|
await confirmOrAbort(
|
|
10166
|
-
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates)?`,
|
|
10470
|
+
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates, ${plan.dlpFallbacks.length} Basic DLP fallbacks losing custom detection)?`,
|
|
10167
10471
|
Boolean(opts.force),
|
|
10168
10472
|
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10169
10473
|
);
|
|
10170
10474
|
const result = await restoreRuntimeProfiles(api, plan);
|
|
10171
|
-
console.log(
|
|
10475
|
+
console.log(renderRestore({ ...result }, columns, format));
|
|
10172
10476
|
if (!result.complete) {
|
|
10173
10477
|
ui.error(result.error ?? "Restore incomplete");
|
|
10174
10478
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs-cli",
|
|
3
3
|
"packageManager": "pnpm@10.6.5",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.7.1",
|
|
5
5
|
"description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"license": "MIT",
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@cdot65/prisma-airs-sdk": "0.
|
|
62
|
+
"@cdot65/prisma-airs-sdk": "0.30.0",
|
|
63
63
|
"@inquirer/prompts": "^8.3.0",
|
|
64
64
|
"chalk": "^5.6.2",
|
|
65
65
|
"commander": "^14.0.3",
|