@cdot65/prisma-airs-cli 5.9.0 → 5.10.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 +460 -85
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -51,7 +51,7 @@ function handleUnhandledRejection(reason) {
|
|
|
51
51
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
52
52
|
console.error(chalk.red(`
|
|
53
53
|
Unexpected error: ${message}`));
|
|
54
|
-
console.error(chalk.dim(" Re-run with --debug to capture
|
|
54
|
+
console.error(chalk.dim(" Re-run with --debug to capture redacted API diagnostics.\n"));
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
57
57
|
function installProcessGuards() {
|
|
@@ -358,7 +358,7 @@ function fail(err) {
|
|
|
358
358
|
\u2717 Error: ${message}`));
|
|
359
359
|
if (status !== void 0) {
|
|
360
360
|
console.error(chalk2.red(` HTTP ${status}`));
|
|
361
|
-
console.error(chalk2.dim(" Re-run with --debug to capture
|
|
361
|
+
console.error(chalk2.dim(" Re-run with --debug to capture redacted API diagnostics."));
|
|
362
362
|
}
|
|
363
363
|
console.error("");
|
|
364
364
|
process.exit(1);
|
|
@@ -8768,6 +8768,7 @@ var topicsView = {
|
|
|
8768
8768
|
// src/cli/commands/dlp/dictionaries.ts
|
|
8769
8769
|
import { readFile as readFile8 } from "fs/promises";
|
|
8770
8770
|
import { basename as basename2 } from "path";
|
|
8771
|
+
import { AISecSDKException as AISecSDKException2, ErrorType as ErrorType3 } from "@cdot65/prisma-airs-sdk";
|
|
8771
8772
|
|
|
8772
8773
|
// src/airs/dlp/dictionaries.ts
|
|
8773
8774
|
var SdkDictionariesService = class {
|
|
@@ -8882,12 +8883,40 @@ function visibleRecords(records, includePredefined) {
|
|
|
8882
8883
|
}
|
|
8883
8884
|
|
|
8884
8885
|
// src/cli/commands/dlp/dictionaries.ts
|
|
8886
|
+
async function readMetadata(path3) {
|
|
8887
|
+
const raw = await readFile8(path3, "utf-8");
|
|
8888
|
+
let value;
|
|
8889
|
+
try {
|
|
8890
|
+
value = JSON.parse(raw);
|
|
8891
|
+
} catch {
|
|
8892
|
+
throw new CliUsageError("Invalid JSON in dictionary metadata file");
|
|
8893
|
+
}
|
|
8894
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8895
|
+
throw new CliUsageError("Dictionary metadata must be a JSON object");
|
|
8896
|
+
}
|
|
8897
|
+
return value;
|
|
8898
|
+
}
|
|
8899
|
+
function dictionaryFailure(err) {
|
|
8900
|
+
if (err instanceof AISecSDKException2 && err.errorType === ErrorType3.USER_REQUEST_PAYLOAD_ERROR) {
|
|
8901
|
+
fail(new CliUsageError("Invalid dictionary metadata: verify required fields and value types"));
|
|
8902
|
+
}
|
|
8903
|
+
fail(err);
|
|
8904
|
+
}
|
|
8905
|
+
function uploadFailure(err) {
|
|
8906
|
+
const error = err;
|
|
8907
|
+
if ((error?.status ?? error?.statusCode) === 400 && !error?.problem?.detail && !error?.problem?.errors?.length) {
|
|
8908
|
+
console.error(
|
|
8909
|
+
'Dictionary upload rejected. Verify the SCM region display name (for example, --region "United States"); region codes such as GLOBAL or us-west-2 are not equivalent.'
|
|
8910
|
+
);
|
|
8911
|
+
}
|
|
8912
|
+
dictionaryFailure(err);
|
|
8913
|
+
}
|
|
8885
8914
|
async function buildMetadata(opts) {
|
|
8886
8915
|
if (opts.metadataFile) {
|
|
8887
|
-
return
|
|
8916
|
+
return await readMetadata(opts.metadataFile);
|
|
8888
8917
|
}
|
|
8889
8918
|
if (!opts.name || !opts.category || !opts.region || !opts.file) {
|
|
8890
|
-
throw new
|
|
8919
|
+
throw new CliUsageError("--name, --category, --region, and --file are required");
|
|
8891
8920
|
}
|
|
8892
8921
|
return {
|
|
8893
8922
|
name: opts.name,
|
|
@@ -8914,7 +8943,7 @@ function register(dlp) {
|
|
|
8914
8943
|
const svc = new SdkDictionariesService(await loadDlpClientOptions());
|
|
8915
8944
|
const params = {
|
|
8916
8945
|
size,
|
|
8917
|
-
sort: opts.sort,
|
|
8946
|
+
sort: opts.sort ?? ["name,asc"],
|
|
8918
8947
|
keywords: includeKeywords ? true : void 0
|
|
8919
8948
|
};
|
|
8920
8949
|
const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : (await svc.list({ ...params, page })).content;
|
|
@@ -8927,11 +8956,11 @@ function register(dlp) {
|
|
|
8927
8956
|
fail(err);
|
|
8928
8957
|
}
|
|
8929
8958
|
});
|
|
8930
|
-
group.command("create").description("Create dictionary via multipart upload").option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "Keyword file").option("--metadata-file <path>", "JSON metadata file (overrides --name/--category/...)").option("--include-keywords", "Include keywords in response").option("--output <fmt>", "Output format", "pretty").action(async (opts, command) => {
|
|
8959
|
+
group.command("create").description("Create dictionary via multipart upload").option("--name <s>", "").option("--category <s>", "").option("--region <s>", 'SCM region display name, e.g. "United States"').option("--description <s>", "").option("--classification <s>", "Legacy top-level metadata field (server support unverified)").option("--file <path>", "Keyword file: TXT (one keyword per line), or CSV (header required)").option("--metadata-file <path>", "JSON metadata file (overrides --name/--category/...)").option("--include-keywords", "Include keywords in response").option("--output <fmt>", "Output format", "pretty").action(async (opts, command) => {
|
|
8931
8960
|
try {
|
|
8932
8961
|
const format = await resolveOutput(command, opts);
|
|
8933
8962
|
const metadata = await buildMetadata(opts);
|
|
8934
|
-
if (!opts.file) throw new
|
|
8963
|
+
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
8935
8964
|
const file = await readFile8(opts.file);
|
|
8936
8965
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
|
|
8937
8966
|
metadata,
|
|
@@ -8940,7 +8969,7 @@ function register(dlp) {
|
|
|
8940
8969
|
});
|
|
8941
8970
|
dlpDictionaries.renderCreated(r, format);
|
|
8942
8971
|
} catch (err) {
|
|
8943
|
-
|
|
8972
|
+
uploadFailure(err);
|
|
8944
8973
|
}
|
|
8945
8974
|
});
|
|
8946
8975
|
const getCmd = group.command("get <id>").option("--keywords", "").option("--include-keywords", "Alias for --keywords").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (id, opts) => {
|
|
@@ -8958,11 +8987,11 @@ function register(dlp) {
|
|
|
8958
8987
|
});
|
|
8959
8988
|
group.command("replace <id>").description(
|
|
8960
8989
|
'Full-replace via multipart upload. --file required. May return 200 (body) or 204 (re-get; falls back to "(state not echoed)" on transient failure).'
|
|
8961
|
-
).option("--name <s>", "").option("--category <s>", "").option("--region <s>", "").option("--description <s>", "").option("--classification <s>", "").option("--file <path>", "
|
|
8990
|
+
).option("--name <s>", "").option("--category <s>", "").option("--region <s>", 'SCM region display name, e.g. "United States"').option("--description <s>", "").option("--classification <s>", "Legacy top-level metadata field (server support unverified)").option("--file <path>", "Required: TXT (one keyword per line), or CSV (header required)").option("--metadata-file <path>", "JSON metadata file").option("--include-keywords", "").option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
|
|
8962
8991
|
try {
|
|
8963
8992
|
const metadata = await buildMetadata(opts);
|
|
8964
8993
|
const format = await resolveOutput(command, opts);
|
|
8965
|
-
if (!opts.file) throw new
|
|
8994
|
+
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
8966
8995
|
const file = await readFile8(opts.file);
|
|
8967
8996
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
|
|
8968
8997
|
metadata,
|
|
@@ -8975,23 +9004,34 @@ function register(dlp) {
|
|
|
8975
9004
|
dlpDictionaries.renderReplaced(r, format);
|
|
8976
9005
|
}
|
|
8977
9006
|
} catch (err) {
|
|
8978
|
-
|
|
9007
|
+
uploadFailure(err);
|
|
8979
9008
|
}
|
|
8980
9009
|
});
|
|
8981
9010
|
group.command("patch <id>").option("--body-file <path>", "").option("--set <k=v...>", "(repeatable)", (v, p = []) => [...p, v]).option("--clear <key...>", "(repeatable)", (v, p = []) => [...p, v]).option("--output <fmt>", "Output format", "pretty").action(async (id, opts, command) => {
|
|
8982
9011
|
try {
|
|
8983
9012
|
const format = await resolveOutput(command, opts);
|
|
8984
9013
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
8985
|
-
throw new
|
|
9014
|
+
throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
|
|
9015
|
+
}
|
|
9016
|
+
let body;
|
|
9017
|
+
if (opts.bodyFile) {
|
|
9018
|
+
body = await readMetadata(opts.bodyFile);
|
|
9019
|
+
} else {
|
|
9020
|
+
try {
|
|
9021
|
+
body = buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
9022
|
+
} catch {
|
|
9023
|
+
throw new CliUsageError(
|
|
9024
|
+
"Invalid dictionary patch flags: use --set key=value, --clear key, or --body-file for nested fields"
|
|
9025
|
+
);
|
|
9026
|
+
}
|
|
8986
9027
|
}
|
|
8987
|
-
const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
8988
9028
|
dlpDictionaries.renderPatched(
|
|
8989
9029
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
8990
9030
|
await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
|
|
8991
9031
|
format
|
|
8992
9032
|
);
|
|
8993
9033
|
} catch (err) {
|
|
8994
|
-
|
|
9034
|
+
dictionaryFailure(err);
|
|
8995
9035
|
}
|
|
8996
9036
|
});
|
|
8997
9037
|
group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
|
|
@@ -9108,31 +9148,70 @@ function buildFilteringProfileBody(opts) {
|
|
|
9108
9148
|
if (opts.fileType?.length) body.file_type = opts.fileType;
|
|
9109
9149
|
return body;
|
|
9110
9150
|
}
|
|
9111
|
-
function
|
|
9151
|
+
function validateProfileType(value) {
|
|
9152
|
+
if (value === "basic") {
|
|
9153
|
+
throw new Error(
|
|
9154
|
+
"Basic profile writes are unsupported: the API returned advanced for basic requests. Use advanced or manage basic profiles in SCM."
|
|
9155
|
+
);
|
|
9156
|
+
}
|
|
9157
|
+
if (value !== void 0 && value !== "advanced") {
|
|
9158
|
+
throw new Error("Profile type must be advanced");
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
function validateProfileName(name) {
|
|
9162
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
9163
|
+
throw new Error("Profile name is required and must be a nonblank string");
|
|
9164
|
+
}
|
|
9165
|
+
if (name.length > 32) throw new Error("Profile name must be at most 32 characters");
|
|
9166
|
+
}
|
|
9167
|
+
function validateProfileFlags(opts) {
|
|
9112
9168
|
if (!opts.name) throw new Error("--name is required");
|
|
9113
|
-
|
|
9169
|
+
validateProfileName(opts.name);
|
|
9170
|
+
validateProfileType(opts.profileType);
|
|
9171
|
+
if (!["and", "or", "not", "and_not", "or_not"].includes((opts.combinator ?? "or").toLowerCase())) {
|
|
9172
|
+
throw new Error("--combinator must be one of and|or|not|and_not|or_not");
|
|
9173
|
+
}
|
|
9174
|
+
if (!["low", "medium", "high"].includes(opts.confidence ?? "high")) {
|
|
9175
|
+
throw new Error("--confidence must be low|medium|high");
|
|
9176
|
+
}
|
|
9177
|
+
}
|
|
9178
|
+
function buildProfileBody(opts, patterns = []) {
|
|
9179
|
+
validateProfileFlags(opts);
|
|
9114
9180
|
const body = {
|
|
9115
9181
|
name: opts.name,
|
|
9116
|
-
profile_type:
|
|
9182
|
+
profile_type: "advanced"
|
|
9117
9183
|
};
|
|
9118
9184
|
if (opts.description != null) body.description = opts.description;
|
|
9119
9185
|
if (opts.granular != null) body.is_granular_data_profile = !!opts.granular;
|
|
9120
9186
|
if (opts.patternId?.length) {
|
|
9121
|
-
const
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9187
|
+
const confidence = opts.confidence ?? "high";
|
|
9188
|
+
const leaves = opts.patternId.map((id) => {
|
|
9189
|
+
const pattern = patterns.find((item) => item.id === id);
|
|
9190
|
+
if (!pattern)
|
|
9191
|
+
throw new Error("Every --pattern-id must resolve before building profile rules");
|
|
9192
|
+
if (pattern.confidenceLevels?.length && !pattern.confidenceLevels.includes(confidence)) {
|
|
9193
|
+
throw new Error("Requested confidence is not supported by every referenced pattern");
|
|
9194
|
+
}
|
|
9195
|
+
return {
|
|
9196
|
+
rule_item: {
|
|
9197
|
+
id: pattern.id,
|
|
9198
|
+
name: pattern.name,
|
|
9199
|
+
version: pattern.version,
|
|
9200
|
+
detection_technique: pattern.technique,
|
|
9201
|
+
confidence_level: confidence,
|
|
9202
|
+
supported_confidence_levels: pattern.confidenceLevels?.length ? [...pattern.confidenceLevels] : [confidence],
|
|
9203
|
+
match_type: "include",
|
|
9204
|
+
occurrence_operator_type: "more_than_equal_to",
|
|
9205
|
+
occurrence_count: 1
|
|
9206
|
+
}
|
|
9207
|
+
};
|
|
9208
|
+
});
|
|
9125
9209
|
body.detection_rules = [
|
|
9126
9210
|
{
|
|
9127
9211
|
rule_type: "expression_tree",
|
|
9128
9212
|
expression_tree: {
|
|
9129
|
-
operator_type:
|
|
9130
|
-
|
|
9131
|
-
data_pattern_id: id,
|
|
9132
|
-
confidence_level: opts.confidence ?? "high",
|
|
9133
|
-
occurrence_operator_type: "any",
|
|
9134
|
-
occurrence_count: 1
|
|
9135
|
-
}))
|
|
9213
|
+
operator_type: (opts.combinator ?? "or").toLowerCase(),
|
|
9214
|
+
sub_expressions: leaves
|
|
9136
9215
|
}
|
|
9137
9216
|
}
|
|
9138
9217
|
];
|
|
@@ -9170,8 +9249,8 @@ function register2(dlp) {
|
|
|
9170
9249
|
try {
|
|
9171
9250
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9172
9251
|
const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
|
|
9173
|
-
const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
|
|
9174
|
-
const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort });
|
|
9252
|
+
const all = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : void 0;
|
|
9253
|
+
const r = all ? { content: all, totalElements: all.length } : await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] });
|
|
9175
9254
|
dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
|
|
9176
9255
|
} catch (err) {
|
|
9177
9256
|
fail(err);
|
|
@@ -9341,7 +9420,7 @@ function register4(dlp) {
|
|
|
9341
9420
|
try {
|
|
9342
9421
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9343
9422
|
const svc = new SdkDataPatternsService(await loadDlpClientOptions());
|
|
9344
|
-
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
|
|
9423
|
+
const result = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] })).content;
|
|
9345
9424
|
const visible = visibleRecords(result, opts.includePredefined);
|
|
9346
9425
|
dlpPatterns.renderList(
|
|
9347
9426
|
{ content: visible, totalElements: visible.length },
|
|
@@ -9419,6 +9498,9 @@ function register4(dlp) {
|
|
|
9419
9498
|
});
|
|
9420
9499
|
}
|
|
9421
9500
|
|
|
9501
|
+
// src/cli/commands/dlp/profiles.ts
|
|
9502
|
+
import { AISecSDKException as AISecSDKException3, ErrorType as ErrorType4 } from "@cdot65/prisma-airs-sdk";
|
|
9503
|
+
|
|
9422
9504
|
// src/airs/dlp/data-profiles.ts
|
|
9423
9505
|
var SdkDataProfilesService = class {
|
|
9424
9506
|
client;
|
|
@@ -9456,7 +9538,7 @@ function listFlags3(cmd) {
|
|
|
9456
9538
|
return cmd;
|
|
9457
9539
|
}
|
|
9458
9540
|
function writeFlags2(cmd) {
|
|
9459
|
-
return cmd.option("--name <s>", "Profile name (required unless --body-file)").option("--profile-type <s>", "Profile type:
|
|
9541
|
+
return cmd.option("--name <s>", "Profile name (required unless --body-file)").option("--profile-type <s>", "Profile type: advanced (basic writes are unsupported)").option("--description <s>", "Description").option("--granular", "Granular data profile").option(
|
|
9460
9542
|
"--pattern-id <id>",
|
|
9461
9543
|
"Data pattern ID to include (repeatable). Builds a simple expression_tree.",
|
|
9462
9544
|
repeatable
|
|
@@ -9468,16 +9550,75 @@ function writeFlags2(cmd) {
|
|
|
9468
9550
|
"Raw JSON body file (escape hatch; required for complex rule trees)"
|
|
9469
9551
|
).option("--output <fmt>", "Output format", "pretty");
|
|
9470
9552
|
}
|
|
9553
|
+
function validateBodyType(body) {
|
|
9554
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
9555
|
+
throw new CliUsageError("Profile body must be a JSON object");
|
|
9556
|
+
}
|
|
9557
|
+
try {
|
|
9558
|
+
validateProfileName(body.name);
|
|
9559
|
+
validateProfileType(body.profile_type);
|
|
9560
|
+
} catch (error) {
|
|
9561
|
+
throw new CliUsageError(error.message);
|
|
9562
|
+
}
|
|
9563
|
+
}
|
|
9564
|
+
function profileFailure(error) {
|
|
9565
|
+
if (error instanceof AISecSDKException3 && error.errorType === ErrorType4.USER_REQUEST_PAYLOAD_ERROR) {
|
|
9566
|
+
fail(new CliUsageError("Invalid profile metadata: verify required fields and value types"));
|
|
9567
|
+
}
|
|
9568
|
+
fail(error);
|
|
9569
|
+
}
|
|
9471
9570
|
async function resolveWriteBody2(opts) {
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9571
|
+
try {
|
|
9572
|
+
validateProfileType(opts.profileType);
|
|
9573
|
+
} catch (error) {
|
|
9574
|
+
throw new CliUsageError(error.message);
|
|
9575
|
+
}
|
|
9576
|
+
if (opts.body !== void 0 || opts.bodyFile !== void 0) {
|
|
9577
|
+
if (opts.body !== void 0 && opts.bodyFile !== void 0) {
|
|
9578
|
+
throw new CliUsageError("--body and --body-file are mutually exclusive");
|
|
9579
|
+
}
|
|
9580
|
+
let body;
|
|
9581
|
+
try {
|
|
9582
|
+
body = await parseBody({ body: opts.body, bodyFile: opts.bodyFile });
|
|
9583
|
+
} catch {
|
|
9584
|
+
throw new CliUsageError("Cannot read profile body as JSON; verify the file and JSON syntax");
|
|
9585
|
+
}
|
|
9586
|
+
validateBodyType(body);
|
|
9478
9587
|
return body;
|
|
9479
9588
|
}
|
|
9480
|
-
|
|
9589
|
+
try {
|
|
9590
|
+
validateProfileFlags(opts);
|
|
9591
|
+
} catch (error) {
|
|
9592
|
+
throw new CliUsageError(error.message);
|
|
9593
|
+
}
|
|
9594
|
+
const patterns = [];
|
|
9595
|
+
const ids = opts.patternId;
|
|
9596
|
+
if (ids?.length) {
|
|
9597
|
+
const svc = new SdkDataPatternsService(await loadDlpClientOptions());
|
|
9598
|
+
for (const id of new Set(ids)) {
|
|
9599
|
+
const pattern = await svc.get(id);
|
|
9600
|
+
if (pattern.id !== id || !pattern.name || !pattern.detection_config?.technique || !Number.isInteger(pattern.version) || Number(pattern.version) < 1) {
|
|
9601
|
+
throw new Error(
|
|
9602
|
+
"Referenced pattern did not return a matching ID, name, technique, and version; no profile was written"
|
|
9603
|
+
);
|
|
9604
|
+
}
|
|
9605
|
+
if (pattern.status !== "active") {
|
|
9606
|
+
throw new Error("Referenced pattern is not active; no profile was written");
|
|
9607
|
+
}
|
|
9608
|
+
patterns.push({
|
|
9609
|
+
id: pattern.id,
|
|
9610
|
+
name: pattern.name,
|
|
9611
|
+
version: pattern.version,
|
|
9612
|
+
technique: pattern.detection_config.technique,
|
|
9613
|
+
confidenceLevels: pattern.detection_config.supported_confidence_levels
|
|
9614
|
+
});
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9617
|
+
try {
|
|
9618
|
+
return buildProfileBody(opts, patterns);
|
|
9619
|
+
} catch (error) {
|
|
9620
|
+
throw new CliUsageError(error.message);
|
|
9621
|
+
}
|
|
9481
9622
|
}
|
|
9482
9623
|
function register5(dlp) {
|
|
9483
9624
|
const group = dlp.command("profiles").description(
|
|
@@ -9490,7 +9631,7 @@ function register5(dlp) {
|
|
|
9490
9631
|
try {
|
|
9491
9632
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9492
9633
|
const svc = new SdkDataProfilesService(await loadDlpClientOptions());
|
|
9493
|
-
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
|
|
9634
|
+
const result = opts.all ? await svc.listAll({ size, sort: opts.sort ?? ["name,asc"], max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort ?? ["name,asc"] })).content;
|
|
9494
9635
|
const visible = visibleRecords(result, opts.includePredefined);
|
|
9495
9636
|
dlpProfiles.renderList(
|
|
9496
9637
|
{ content: visible, totalElements: visible.length },
|
|
@@ -9511,7 +9652,7 @@ function register5(dlp) {
|
|
|
9511
9652
|
format
|
|
9512
9653
|
);
|
|
9513
9654
|
} catch (err) {
|
|
9514
|
-
|
|
9655
|
+
profileFailure(err);
|
|
9515
9656
|
}
|
|
9516
9657
|
}
|
|
9517
9658
|
);
|
|
@@ -9536,7 +9677,7 @@ function register5(dlp) {
|
|
|
9536
9677
|
format
|
|
9537
9678
|
);
|
|
9538
9679
|
} catch (err) {
|
|
9539
|
-
|
|
9680
|
+
profileFailure(err);
|
|
9540
9681
|
}
|
|
9541
9682
|
}
|
|
9542
9683
|
);
|
|
@@ -9546,16 +9687,24 @@ function register5(dlp) {
|
|
|
9546
9687
|
try {
|
|
9547
9688
|
const format = await resolveOutput(command, opts);
|
|
9548
9689
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
9549
|
-
throw new
|
|
9690
|
+
throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
|
|
9550
9691
|
}
|
|
9551
|
-
|
|
9692
|
+
let body;
|
|
9693
|
+
try {
|
|
9694
|
+
body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
9695
|
+
} catch {
|
|
9696
|
+
throw new CliUsageError(
|
|
9697
|
+
"Invalid profile patch: verify JSON syntax and --set key=value/--clear key flags"
|
|
9698
|
+
);
|
|
9699
|
+
}
|
|
9700
|
+
validateBodyType(body);
|
|
9552
9701
|
dlpProfiles.renderPatched(
|
|
9553
9702
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
9554
9703
|
await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
|
|
9555
9704
|
format
|
|
9556
9705
|
);
|
|
9557
9706
|
} catch (err) {
|
|
9558
|
-
|
|
9707
|
+
profileFailure(err);
|
|
9559
9708
|
}
|
|
9560
9709
|
});
|
|
9561
9710
|
group.command("delete <id>").description("Not supported \u2014 explains the cleanup limitation and exits 2").action((id) => {
|
|
@@ -9573,7 +9722,7 @@ function register5(dlp) {
|
|
|
9573
9722
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
9574
9723
|
import { readFile as readFile9, stat } from "fs/promises";
|
|
9575
9724
|
import { extname, resolve as resolve5 } from "path";
|
|
9576
|
-
import { AISecSDKException as
|
|
9725
|
+
import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9577
9726
|
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
9578
9727
|
import { ZodError } from "zod";
|
|
9579
9728
|
|
|
@@ -9632,13 +9781,79 @@ function assertName(name) {
|
|
|
9632
9781
|
async function springPages(fetch, maxPages3, label) {
|
|
9633
9782
|
pagesLimit(maxPages3);
|
|
9634
9783
|
const items = [];
|
|
9784
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9785
|
+
let totalPages;
|
|
9786
|
+
let totalElements;
|
|
9787
|
+
let effectiveSize;
|
|
9788
|
+
const inconsistent = () => {
|
|
9789
|
+
throw new Error(
|
|
9790
|
+
`Inventory of ${label} has inconsistent pagination metadata; retry the operation`
|
|
9791
|
+
);
|
|
9792
|
+
};
|
|
9793
|
+
const alias = (camel, snake) => {
|
|
9794
|
+
if (camel !== void 0 && snake !== void 0 && camel !== snake) inconsistent();
|
|
9795
|
+
return camel ?? snake;
|
|
9796
|
+
};
|
|
9635
9797
|
for (let page = 0; page < maxPages3; page++) {
|
|
9636
9798
|
const result = await fetch(page);
|
|
9637
|
-
|
|
9638
|
-
|
|
9799
|
+
const pageSize = alias(result.pageable?.pageSize, result.pageable?.page_size);
|
|
9800
|
+
const suppliedSize = alias(result.size, pageSize);
|
|
9801
|
+
if (suppliedSize !== void 0 && (!Number.isSafeInteger(suppliedSize) || suppliedSize < 1 || suppliedSize > 100 || effectiveSize !== void 0 && suppliedSize !== effectiveSize))
|
|
9802
|
+
inconsistent();
|
|
9803
|
+
effectiveSize = suppliedSize ?? effectiveSize ?? 100;
|
|
9804
|
+
const suppliedPages = alias(result.totalPages, result.total_pages);
|
|
9805
|
+
const suppliedElements = alias(result.totalElements, result.total_elements);
|
|
9806
|
+
if (items.length + result.content.length > 1e4)
|
|
9639
9807
|
throw new Error(`Inventory of ${label} exceeds the 10000-record safety limit`);
|
|
9640
|
-
|
|
9641
|
-
|
|
9808
|
+
for (const [actual, expected] of [
|
|
9809
|
+
[result.number, page],
|
|
9810
|
+
[result.size, effectiveSize],
|
|
9811
|
+
[alias(result.numberOfElements, result.number_of_elements), result.content.length],
|
|
9812
|
+
[result.first, page === 0],
|
|
9813
|
+
[result.empty, result.content.length === 0],
|
|
9814
|
+
[alias(result.pageable?.pageNumber, result.pageable?.page_number), page],
|
|
9815
|
+
[pageSize, effectiveSize],
|
|
9816
|
+
[result.pageable?.offset, page * effectiveSize]
|
|
9817
|
+
]) {
|
|
9818
|
+
if (actual !== void 0 && actual !== expected) inconsistent();
|
|
9819
|
+
}
|
|
9820
|
+
for (const [actual, previous] of [
|
|
9821
|
+
[suppliedPages, totalPages],
|
|
9822
|
+
[suppliedElements, totalElements]
|
|
9823
|
+
]) {
|
|
9824
|
+
if (actual !== void 0 && (!Number.isSafeInteger(actual) || actual < 0 || previous !== void 0 && actual !== previous))
|
|
9825
|
+
inconsistent();
|
|
9826
|
+
}
|
|
9827
|
+
totalPages = suppliedPages ?? totalPages;
|
|
9828
|
+
totalElements = suppliedElements ?? totalElements;
|
|
9829
|
+
if (totalPages !== void 0 && totalElements !== void 0 && totalPages !== Math.ceil(totalElements / effectiveSize))
|
|
9830
|
+
inconsistent();
|
|
9831
|
+
if (totalPages !== void 0 && (page >= Math.max(totalPages, 1) || totalPages === 0 && result.content.length !== 0))
|
|
9832
|
+
inconsistent();
|
|
9833
|
+
if (totalElements !== void 0 && result.content.length !== Math.min(effectiveSize, Math.max(0, totalElements - page * effectiveSize)))
|
|
9834
|
+
inconsistent();
|
|
9835
|
+
for (const item of result.content) {
|
|
9836
|
+
if (typeof item.id !== "string" || !item.id.trim())
|
|
9837
|
+
throw new Error(
|
|
9838
|
+
`Inventory of ${label} contains a missing resource ID; retry the operation`
|
|
9839
|
+
);
|
|
9840
|
+
if (ids.has(item.id))
|
|
9841
|
+
throw new Error(
|
|
9842
|
+
`Inventory of ${label} contains duplicate resource IDs; retry the operation`
|
|
9843
|
+
);
|
|
9844
|
+
ids.add(item.id);
|
|
9845
|
+
items.push(item);
|
|
9846
|
+
}
|
|
9847
|
+
const expectedLast = totalPages !== void 0 ? page + 1 >= totalPages : totalElements !== void 0 ? items.length === totalElements : void 0;
|
|
9848
|
+
if (result.last !== void 0 && expectedLast !== void 0 && result.last !== expectedLast)
|
|
9849
|
+
inconsistent();
|
|
9850
|
+
const last = result.last ?? expectedLast ?? result.content.length < effectiveSize;
|
|
9851
|
+
if (totalPages !== void 0 && totalPages > 0 && (!last && result.content.length !== effectiveSize || last && result.content.length === 0))
|
|
9852
|
+
inconsistent();
|
|
9853
|
+
if (last) {
|
|
9854
|
+
if (totalElements !== void 0 && items.length !== totalElements) inconsistent();
|
|
9855
|
+
return items;
|
|
9856
|
+
}
|
|
9642
9857
|
if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
|
|
9643
9858
|
}
|
|
9644
9859
|
throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
|
|
@@ -9646,17 +9861,17 @@ async function springPages(fetch, maxPages3, label) {
|
|
|
9646
9861
|
async function inventories(api, maxPages3) {
|
|
9647
9862
|
return {
|
|
9648
9863
|
dictionaries: await springPages(
|
|
9649
|
-
(page) => api.dictionaries.list({ page, size: 100, keywords: true }),
|
|
9864
|
+
(page) => api.dictionaries.list({ page, size: 100, sort: ["name,asc"], keywords: true }),
|
|
9650
9865
|
maxPages3,
|
|
9651
9866
|
"dictionaries"
|
|
9652
9867
|
),
|
|
9653
9868
|
patterns: await springPages(
|
|
9654
|
-
(page) => api.patterns.list({ page, size: 100 }),
|
|
9869
|
+
(page) => api.patterns.list({ page, size: 100, sort: ["name,asc"] }),
|
|
9655
9870
|
maxPages3,
|
|
9656
9871
|
"data patterns"
|
|
9657
9872
|
),
|
|
9658
9873
|
profiles: await springPages(
|
|
9659
|
-
(page) => api.profiles.list({ page, size: 100 }),
|
|
9874
|
+
(page) => api.profiles.list({ page, size: 100, sort: ["name,asc"] }),
|
|
9660
9875
|
maxPages3,
|
|
9661
9876
|
"data profiles"
|
|
9662
9877
|
)
|
|
@@ -9786,12 +10001,14 @@ async function backupDlpResources(api, tsgId, options = {}) {
|
|
|
9786
10001
|
if (leaf.technique === "dictionary") {
|
|
9787
10002
|
const record = dictionaryById.get(id);
|
|
9788
10003
|
if (!record) throw new Error(`references an unknown dictionary: ${id}`);
|
|
9789
|
-
if (leaf.value.version != null)
|
|
10004
|
+
if (leaf.value.version != null && leaf.value.version !== 1)
|
|
9790
10005
|
throw new Error(`references a version-pinned dictionary: ${id}`);
|
|
9791
10006
|
addDictionary(record);
|
|
9792
10007
|
} else {
|
|
9793
10008
|
const record = patternById.get(id);
|
|
9794
10009
|
if (!record) throw new Error(`references an unknown data pattern: ${id}`);
|
|
10010
|
+
if (record.detection_config?.technique !== leaf.technique)
|
|
10011
|
+
throw new Error("references a data pattern with a different detection technique");
|
|
9795
10012
|
if (RETIRED_PATTERN.has(record.status ?? "active"))
|
|
9796
10013
|
throw new Error(`references a retired data pattern: ${record.name ?? id}`);
|
|
9797
10014
|
if (leaf.value.version != null && record.version !== leaf.value.version)
|
|
@@ -9878,6 +10095,20 @@ function keywordSet(value) {
|
|
|
9878
10095
|
return void 0;
|
|
9879
10096
|
return [...value].sort();
|
|
9880
10097
|
}
|
|
10098
|
+
function dictionaryFile(metadata, keywords) {
|
|
10099
|
+
if (!keywords.length || keywords.some((word) => !word.length || /[\r\n]/.test(word)))
|
|
10100
|
+
throw new Error("Dictionary contains keywords that cannot be faithfully uploaded");
|
|
10101
|
+
if (/\.csv$/i.test(metadata.original_file_name)) {
|
|
10102
|
+
if (keywords.some((word) => word.includes(",")))
|
|
10103
|
+
throw new Error("Dictionary CSV contains unsupported multi-column keywords");
|
|
10104
|
+
return `keywords
|
|
10105
|
+
${keywords.join("\n")}
|
|
10106
|
+
`;
|
|
10107
|
+
}
|
|
10108
|
+
if (/\.txt$/i.test(metadata.original_file_name)) return `${keywords.join("\n")}
|
|
10109
|
+
`;
|
|
10110
|
+
throw new Error("Dictionary filename must end in .csv or .txt for a verified upload");
|
|
10111
|
+
}
|
|
9881
10112
|
function dictionaryEcho(metadata, record, keywords) {
|
|
9882
10113
|
const { original_file_name, ...projection } = metadata;
|
|
9883
10114
|
const echo = compareEcho(projection, record);
|
|
@@ -9893,7 +10124,8 @@ function dictionaryRequest(source, name) {
|
|
|
9893
10124
|
original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
|
|
9894
10125
|
region_name: source.region_name,
|
|
9895
10126
|
...source.description != null ? { description: source.description } : {},
|
|
9896
|
-
...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {}
|
|
10127
|
+
...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {},
|
|
10128
|
+
...source.tags != null ? { tags: structuredClone(source.tags) } : {}
|
|
9897
10129
|
};
|
|
9898
10130
|
const parsed = DictionaryRequestSchema.safeParse(metadata);
|
|
9899
10131
|
if (!parsed.success)
|
|
@@ -9946,11 +10178,30 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
9946
10178
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9947
10179
|
throw new Error("Invalid --name-prefix");
|
|
9948
10180
|
const conflict = options.onConflict ?? "error";
|
|
9949
|
-
if (!["error", "verify", "skip"].includes(conflict))
|
|
10181
|
+
if (!["error", "verify", "skip", "reconcile"].includes(conflict))
|
|
10182
|
+
throw new Error("Invalid conflict policy");
|
|
9950
10183
|
const patternMap2 = options.patternMap ?? {};
|
|
9951
10184
|
uniqueNames(backup.dictionaries, "dictionary");
|
|
9952
10185
|
uniqueNames(backup.patterns, "data pattern");
|
|
9953
10186
|
uniqueNames(backup.profiles, "data profile");
|
|
10187
|
+
const sourceBindings = /* @__PURE__ */ new Map();
|
|
10188
|
+
for (const [kind, records] of [
|
|
10189
|
+
["dictionary", backup.dictionaries],
|
|
10190
|
+
["pattern", backup.patterns]
|
|
10191
|
+
]) {
|
|
10192
|
+
for (const record of records) {
|
|
10193
|
+
if (!record.id) continue;
|
|
10194
|
+
if (sourceBindings.has(record.id))
|
|
10195
|
+
throw new Error("Backup contains duplicate dependency identities");
|
|
10196
|
+
sourceBindings.set(record.id, {
|
|
10197
|
+
id: record.id,
|
|
10198
|
+
name: record.name ?? "",
|
|
10199
|
+
kind,
|
|
10200
|
+
technique: kind === "dictionary" ? "dictionary" : record.detection_config?.technique,
|
|
10201
|
+
version: kind === "dictionary" ? 1 : record.version ?? void 0
|
|
10202
|
+
});
|
|
10203
|
+
}
|
|
10204
|
+
}
|
|
9954
10205
|
const unknownMapped = Object.keys(patternMap2).filter(
|
|
9955
10206
|
(name) => !backup.patterns.some((p) => p.name === name)
|
|
9956
10207
|
);
|
|
@@ -10004,6 +10255,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10004
10255
|
if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
|
|
10005
10256
|
const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
|
|
10006
10257
|
if (!match) {
|
|
10258
|
+
dictionaryFile(metadata, keywords);
|
|
10007
10259
|
dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
|
|
10008
10260
|
continue;
|
|
10009
10261
|
}
|
|
@@ -10044,6 +10296,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10044
10296
|
else missingPredefined.push({ name: source.name, candidates });
|
|
10045
10297
|
continue;
|
|
10046
10298
|
}
|
|
10299
|
+
if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
|
|
10300
|
+
throw new Error(`Destination data pattern detection technique differs: ${source.name}`);
|
|
10047
10301
|
patternPlans.push({
|
|
10048
10302
|
sourceId: source.id,
|
|
10049
10303
|
name: match2.name,
|
|
@@ -10056,6 +10310,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10056
10310
|
const target = patternMap2[source.name];
|
|
10057
10311
|
const match2 = matchByName(destination.patterns, target, retired, "data pattern");
|
|
10058
10312
|
if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
|
|
10313
|
+
if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
|
|
10314
|
+
throw new Error(`Mapped destination data pattern detection technique differs: ${target}`);
|
|
10059
10315
|
patternPlans.push({
|
|
10060
10316
|
sourceId: source.id,
|
|
10061
10317
|
name: target,
|
|
@@ -10151,6 +10407,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10151
10407
|
patterns: patternPlans,
|
|
10152
10408
|
profiles: profilePlans,
|
|
10153
10409
|
unresolved,
|
|
10410
|
+
suffixOnConflict: conflict === "reconcile",
|
|
10154
10411
|
maxPages: maxPages3
|
|
10155
10412
|
};
|
|
10156
10413
|
const bindings = planBindings(plan);
|
|
@@ -10164,7 +10421,15 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10164
10421
|
const missing = /* @__PURE__ */ new Set();
|
|
10165
10422
|
for (const leaf of ruleLeaves(item.source)) {
|
|
10166
10423
|
const id = leaf.value.id;
|
|
10167
|
-
if (id
|
|
10424
|
+
if (id == null || id === "") continue;
|
|
10425
|
+
if (typeof id !== "string") throw new Error("Backup contains a non-string rule reference ID");
|
|
10426
|
+
const sourceBinding = sourceBindings.get(id);
|
|
10427
|
+
if (sourceBinding) {
|
|
10428
|
+
assertLeafBinding(leaf, sourceBinding);
|
|
10429
|
+
if (leaf.value.version != null && leaf.value.version !== sourceBinding.version)
|
|
10430
|
+
throw new Error("Backup rule pins an unknown source dependency version");
|
|
10431
|
+
}
|
|
10432
|
+
if (!planned.has(id)) missing.add(id);
|
|
10168
10433
|
}
|
|
10169
10434
|
if (missing.size) {
|
|
10170
10435
|
const skippable = [...missing].filter((id) => unresolvedById.has(id));
|
|
@@ -10185,20 +10450,39 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10185
10450
|
}
|
|
10186
10451
|
return plan;
|
|
10187
10452
|
}
|
|
10453
|
+
function assertLeafBinding(leaf, binding) {
|
|
10454
|
+
if (leaf.technique === "dictionary" !== (binding.kind === "dictionary") || leaf.technique !== binding.technique)
|
|
10455
|
+
throw new Error("Data profile rule detection technique does not match its dependency");
|
|
10456
|
+
}
|
|
10188
10457
|
function planBindings(plan) {
|
|
10189
10458
|
const bindings = /* @__PURE__ */ new Map();
|
|
10190
10459
|
for (const item of plan.dictionaries)
|
|
10191
|
-
if (item.existing?.id)
|
|
10460
|
+
if (item.existing?.id)
|
|
10461
|
+
bindings.set(item.sourceId, {
|
|
10462
|
+
id: item.existing.id,
|
|
10463
|
+
name: item.name,
|
|
10464
|
+
version: 1,
|
|
10465
|
+
kind: "dictionary",
|
|
10466
|
+
technique: "dictionary"
|
|
10467
|
+
});
|
|
10192
10468
|
for (const item of plan.patterns)
|
|
10193
10469
|
if (item.existing?.id)
|
|
10194
10470
|
bindings.set(item.sourceId, {
|
|
10195
10471
|
id: item.existing.id,
|
|
10196
10472
|
name: item.name,
|
|
10473
|
+
kind: "pattern",
|
|
10474
|
+
technique: item.existing.detection_config?.technique,
|
|
10197
10475
|
version: item.existing.version ?? void 0
|
|
10198
10476
|
});
|
|
10199
10477
|
return bindings;
|
|
10200
10478
|
}
|
|
10201
10479
|
function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
10480
|
+
if (item.action === "create" && item.name.length > 32)
|
|
10481
|
+
throw new Error("Data profile name exceeds the live API limit of 32 characters");
|
|
10482
|
+
if (item.action === "create" && item.source.profile_type === "basic")
|
|
10483
|
+
throw new Error("Data profile basic writes cannot round-trip through the API");
|
|
10484
|
+
if (item.action === "create" && !item.source.detection_rules?.length)
|
|
10485
|
+
throw new Error("Data profile has no exported detection rules to create");
|
|
10202
10486
|
const detectionRules = structuredClone(item.source.detection_rules ?? []);
|
|
10203
10487
|
const known = /* @__PURE__ */ new Set();
|
|
10204
10488
|
let deferred = false;
|
|
@@ -10207,6 +10491,8 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10207
10491
|
known.add(leaf.value);
|
|
10208
10492
|
const id = leaf.value.id;
|
|
10209
10493
|
if (id == null || id === "") continue;
|
|
10494
|
+
if (leaf.technique === "dictionary" && leaf.value.version != null && leaf.value.version !== 1)
|
|
10495
|
+
throw new Error("Dictionary rule has an unsupported version pin");
|
|
10210
10496
|
const binding = bindings.get(String(id));
|
|
10211
10497
|
if (!binding) {
|
|
10212
10498
|
if (requireBindings)
|
|
@@ -10216,6 +10502,7 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10216
10502
|
deferred = true;
|
|
10217
10503
|
continue;
|
|
10218
10504
|
}
|
|
10505
|
+
assertLeafBinding(leaf, binding);
|
|
10219
10506
|
leaf.value.id = binding.id;
|
|
10220
10507
|
if (leaf.value.name != null) leaf.value.name = binding.name;
|
|
10221
10508
|
if (leaf.value.version != null) {
|
|
@@ -10245,6 +10532,15 @@ function assertUnchanged(current, prior, label) {
|
|
|
10245
10532
|
if (canonical(current ?? null) !== canonical(prior ?? null))
|
|
10246
10533
|
throw new Error(`Destination ${label} changed after planning`);
|
|
10247
10534
|
}
|
|
10535
|
+
function defaultProfileSuffix() {
|
|
10536
|
+
return Math.floor(Math.random() * 16777216).toString(16).padStart(6, "0");
|
|
10537
|
+
}
|
|
10538
|
+
function suffixedProfileName(name, suffix) {
|
|
10539
|
+
const tail = `-${suffix}`;
|
|
10540
|
+
const room = 32 - tail.length;
|
|
10541
|
+
const base = name.length > room ? name.slice(0, room) : name;
|
|
10542
|
+
return `${base}${tail}`;
|
|
10543
|
+
}
|
|
10248
10544
|
async function restoreDlpResources(api, plan, options = {}) {
|
|
10249
10545
|
const result = {
|
|
10250
10546
|
sourceTsgId: plan.sourceTsgId,
|
|
@@ -10254,10 +10550,12 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10254
10550
|
patterns: [],
|
|
10255
10551
|
profiles: [],
|
|
10256
10552
|
serverAdded: [],
|
|
10553
|
+
unverifiedCreates: [],
|
|
10257
10554
|
unresolved: structuredClone(plan.unresolved ?? [])
|
|
10258
10555
|
};
|
|
10259
10556
|
const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
|
|
10260
10557
|
const emit2 = options.onProgress;
|
|
10558
|
+
const suffixGenerator = options.suffixGenerator ?? defaultProfileSuffix;
|
|
10261
10559
|
const emitItem = (stage, name, action, index, total) => emit2?.({ phase: "item", stage, name, action, index, total });
|
|
10262
10560
|
try {
|
|
10263
10561
|
emit2?.({ phase: "recheck" });
|
|
@@ -10306,21 +10604,28 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10306
10604
|
if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
|
|
10307
10605
|
const created = await api.dictionaries.create({
|
|
10308
10606
|
metadata: item.metadata,
|
|
10309
|
-
file:
|
|
10310
|
-
`,
|
|
10607
|
+
file: dictionaryFile(item.metadata, item.keywords),
|
|
10311
10608
|
includeKeywords: true
|
|
10312
10609
|
});
|
|
10313
10610
|
if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10611
|
+
result.unverifiedCreates.push({ kind: "dictionaries", name: item.name, id: created.id });
|
|
10314
10612
|
const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
|
|
10315
10613
|
const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
|
|
10316
|
-
if (!echo.matches || readBack.name !== item.name)
|
|
10614
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name)
|
|
10317
10615
|
throw new Error(
|
|
10318
10616
|
`Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10319
10617
|
);
|
|
10320
10618
|
if (echo.serverAdded.length)
|
|
10321
10619
|
result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
|
|
10322
|
-
bindings.set(item.sourceId, {
|
|
10620
|
+
bindings.set(item.sourceId, {
|
|
10621
|
+
id: created.id,
|
|
10622
|
+
name: item.name,
|
|
10623
|
+
version: 1,
|
|
10624
|
+
kind: "dictionary",
|
|
10625
|
+
technique: "dictionary"
|
|
10626
|
+
});
|
|
10323
10627
|
result.dictionaries.push({ name: item.name, action: "created", id: created.id });
|
|
10628
|
+
result.unverifiedCreates.pop();
|
|
10324
10629
|
emitItem(
|
|
10325
10630
|
"dictionaries",
|
|
10326
10631
|
item.name,
|
|
@@ -10348,9 +10653,10 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10348
10653
|
if (!item.body) throw new Error("Data pattern plan is incomplete");
|
|
10349
10654
|
const created = await api.patterns.create(item.body);
|
|
10350
10655
|
if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10656
|
+
result.unverifiedCreates.push({ kind: "patterns", name: item.name, id: created.id });
|
|
10351
10657
|
const readBack = await api.patterns.get(created.id);
|
|
10352
10658
|
const echo = compareEcho(item.body, readBack);
|
|
10353
|
-
if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10659
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10354
10660
|
throw new Error(
|
|
10355
10661
|
`Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10356
10662
|
);
|
|
@@ -10362,9 +10668,12 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10362
10668
|
bindings.set(item.sourceId, {
|
|
10363
10669
|
id: created.id,
|
|
10364
10670
|
name: item.name,
|
|
10671
|
+
kind: "pattern",
|
|
10672
|
+
technique: readBack.detection_config?.technique,
|
|
10365
10673
|
version: readBack.version ?? void 0
|
|
10366
10674
|
});
|
|
10367
10675
|
result.patterns.push({ name: item.name, action: "created", id: created.id });
|
|
10676
|
+
result.unverifiedCreates.pop();
|
|
10368
10677
|
emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
|
|
10369
10678
|
}
|
|
10370
10679
|
if (plan.profiles.length)
|
|
@@ -10399,21 +10708,39 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10399
10708
|
emitItem("profiles", item.name, "verified", result.profiles.length, plan.profiles.length);
|
|
10400
10709
|
continue;
|
|
10401
10710
|
}
|
|
10402
|
-
|
|
10403
|
-
|
|
10711
|
+
let writeBody = body;
|
|
10712
|
+
let finalName = item.name;
|
|
10713
|
+
let created;
|
|
10714
|
+
try {
|
|
10715
|
+
created = await api.profiles.create(writeBody);
|
|
10716
|
+
} catch (error) {
|
|
10717
|
+
if (!plan.suffixOnConflict || error?.statusCode !== 409)
|
|
10718
|
+
throw error;
|
|
10719
|
+
finalName = suffixedProfileName(item.name, suffixGenerator());
|
|
10720
|
+
writeBody = { ...body, name: finalName };
|
|
10721
|
+
created = await api.profiles.create(writeBody);
|
|
10722
|
+
}
|
|
10723
|
+
if (!created.id) throw new Error(`Restored data profile did not verify: ${finalName}`);
|
|
10724
|
+
result.unverifiedCreates.push({ kind: "profiles", name: finalName, id: created.id });
|
|
10404
10725
|
const readBack = await api.profiles.get(created.id);
|
|
10405
|
-
const echo = compareEcho(
|
|
10406
|
-
if (!echo.matches || readBack.
|
|
10726
|
+
const echo = compareEcho(writeBody, readBack);
|
|
10727
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== finalName || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10407
10728
|
throw new Error(
|
|
10408
|
-
`Restored data profile did not verify: ${
|
|
10729
|
+
`Restored data profile did not verify: ${finalName} (created id ${created.id}; inspect it before retrying)`
|
|
10409
10730
|
);
|
|
10410
10731
|
if (echo.serverAdded.length)
|
|
10411
10732
|
result.serverAdded.push({
|
|
10412
|
-
resource: `data profile ${
|
|
10733
|
+
resource: `data profile ${finalName}`,
|
|
10413
10734
|
fields: echo.serverAdded
|
|
10414
10735
|
});
|
|
10415
|
-
result.profiles.push({
|
|
10416
|
-
|
|
10736
|
+
result.profiles.push({
|
|
10737
|
+
name: finalName,
|
|
10738
|
+
action: "created",
|
|
10739
|
+
id: created.id,
|
|
10740
|
+
...finalName !== item.name ? { renamedFrom: item.name } : {}
|
|
10741
|
+
});
|
|
10742
|
+
result.unverifiedCreates.pop();
|
|
10743
|
+
emitItem("profiles", finalName, "created", result.profiles.length, plan.profiles.length);
|
|
10417
10744
|
}
|
|
10418
10745
|
result.complete = true;
|
|
10419
10746
|
} catch (error) {
|
|
@@ -10421,6 +10748,9 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10421
10748
|
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(
|
|
10422
10749
|
error.message
|
|
10423
10750
|
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
10751
|
+
const unverified = result.unverifiedCreates[0];
|
|
10752
|
+
if (unverified)
|
|
10753
|
+
result.error += `; unverified create: ${unverified.kind} ${unverified.name} (id ${unverified.id}); inspect this resource before retrying`;
|
|
10424
10754
|
}
|
|
10425
10755
|
return result;
|
|
10426
10756
|
}
|
|
@@ -10470,7 +10800,7 @@ function failure(error) {
|
|
|
10470
10800
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
10471
10801
|
if (error instanceof ZodError) {
|
|
10472
10802
|
ui.error("Invalid DLP backup/request schema; content is not logged.");
|
|
10473
|
-
} else if (error instanceof
|
|
10803
|
+
} else if (error instanceof AISecSDKException4) {
|
|
10474
10804
|
ui.error(
|
|
10475
10805
|
`DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10476
10806
|
);
|
|
@@ -10544,6 +10874,25 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
|
10544
10874
|
for (const entry of rows("unresolved"))
|
|
10545
10875
|
blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
|
|
10546
10876
|
}
|
|
10877
|
+
if (rows("unverifiedCreates").length) {
|
|
10878
|
+
blocks.push(heading("Created resources awaiting verification"));
|
|
10879
|
+
blocks.push(
|
|
10880
|
+
formatOutput(
|
|
10881
|
+
rows("unverifiedCreates"),
|
|
10882
|
+
[
|
|
10883
|
+
{ key: "kind", label: "Resource" },
|
|
10884
|
+
{ key: "name", label: "Name" },
|
|
10885
|
+
{ key: "id", label: "Created ID" }
|
|
10886
|
+
],
|
|
10887
|
+
markdown ? "markdown" : "table"
|
|
10888
|
+
)
|
|
10889
|
+
);
|
|
10890
|
+
}
|
|
10891
|
+
const renamed = rows("profiles").filter((p) => p.renamedFrom);
|
|
10892
|
+
if (renamed.length) {
|
|
10893
|
+
blocks.push(heading("Renamed around archived-name collisions"));
|
|
10894
|
+
for (const p of renamed) blocks.push(`${p.renamedFrom} -> ${p.name}`);
|
|
10895
|
+
}
|
|
10547
10896
|
if (rows("serverAdded").length) {
|
|
10548
10897
|
blocks.push(heading("Verified server-added fields (source omitted these)"));
|
|
10549
10898
|
for (const item of rows("serverAdded"))
|
|
@@ -10637,7 +10986,7 @@ function register6(dlp) {
|
|
|
10637
10986
|
"Restore DLP resources into the selected tenant, staged dictionaries \u2192 patterns \u2192 profiles; validate all dependencies before changes"
|
|
10638
10987
|
).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(
|
|
10639
10988
|
"--on-conflict <policy>",
|
|
10640
|
-
"Existing data profile names: error, verify (resume without writes) or
|
|
10989
|
+
"Existing data profile names: error, verify (resume without writes), skip, or reconcile (verify active duplicates, and give archived-name collisions a unique suffix). Profile update is a live server error, so a divergent active profile still fails.",
|
|
10641
10990
|
"error"
|
|
10642
10991
|
).option(
|
|
10643
10992
|
"--pattern-map <source=destination>",
|
|
@@ -10664,8 +11013,8 @@ function register6(dlp) {
|
|
|
10664
11013
|
)
|
|
10665
11014
|
).action(async (file, opts) => {
|
|
10666
11015
|
try {
|
|
10667
|
-
if (!["error", "verify", "skip"].includes(opts.onConflict))
|
|
10668
|
-
throw new CliUsageError("--on-conflict must be error, verify or
|
|
11016
|
+
if (!["error", "verify", "skip", "reconcile"].includes(opts.onConflict))
|
|
11017
|
+
throw new CliUsageError("--on-conflict must be error, verify, skip or reconcile");
|
|
10669
11018
|
if (opts.force && !opts.expectTsg)
|
|
10670
11019
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10671
11020
|
const mappings = patternMap(opts.patternMap);
|
|
@@ -10728,6 +11077,7 @@ function register6(dlp) {
|
|
|
10728
11077
|
{ key: "profiles", label: "Data profiles" },
|
|
10729
11078
|
{ key: "unresolved", label: "Unresolved references" },
|
|
10730
11079
|
{ key: "serverAdded", label: "Verified server-added fields" },
|
|
11080
|
+
{ key: "unverifiedCreates", label: "Created resources awaiting verification" },
|
|
10731
11081
|
{ key: "dryRun", label: "Dry run" },
|
|
10732
11082
|
{ key: "complete", label: "Complete" }
|
|
10733
11083
|
];
|
|
@@ -10771,7 +11121,7 @@ function registerDlpCommands(runtime) {
|
|
|
10771
11121
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
10772
11122
|
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
10773
11123
|
import { extname as extname2, resolve as resolve6 } from "path";
|
|
10774
|
-
import { AISecSDKException as
|
|
11124
|
+
import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
10775
11125
|
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
10776
11126
|
import { ZodError as ZodError2 } from "zod";
|
|
10777
11127
|
|
|
@@ -11492,7 +11842,7 @@ function failure2(error) {
|
|
|
11492
11842
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
11493
11843
|
if (error instanceof ZodError2) {
|
|
11494
11844
|
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
11495
|
-
} else if (error instanceof
|
|
11845
|
+
} else if (error instanceof AISecSDKException5) {
|
|
11496
11846
|
ui.error(
|
|
11497
11847
|
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
11498
11848
|
);
|
|
@@ -13689,12 +14039,41 @@ function buildProgram() {
|
|
|
13689
14039
|
const here = dirname5(fileURLToPath(import.meta.url));
|
|
13690
14040
|
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
13691
14041
|
const program = new Command();
|
|
14042
|
+
let dlpDebugBodyEnvironment;
|
|
14043
|
+
const restoreDlpDebugBody = () => {
|
|
14044
|
+
if (!dlpDebugBodyEnvironment) return;
|
|
14045
|
+
const { previous } = dlpDebugBodyEnvironment;
|
|
14046
|
+
if (previous === void 0) delete process.env.PANW_AI_SEC_DEBUG_BODY;
|
|
14047
|
+
else process.env.PANW_AI_SEC_DEBUG_BODY = previous;
|
|
14048
|
+
dlpDebugBodyEnvironment = void 0;
|
|
14049
|
+
};
|
|
14050
|
+
program.hook("postAction", restoreDlpDebugBody);
|
|
14051
|
+
const parseAsync = program.parseAsync.bind(program);
|
|
14052
|
+
program.parseAsync = async (...args) => {
|
|
14053
|
+
try {
|
|
14054
|
+
return await parseAsync(...args);
|
|
14055
|
+
} finally {
|
|
14056
|
+
restoreDlpDebugBody();
|
|
14057
|
+
}
|
|
14058
|
+
};
|
|
13692
14059
|
program.name("airs").description(
|
|
13693
14060
|
"CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
|
|
13694
|
-
).version(pkg.version).option("--debug", "
|
|
14061
|
+
).version(pkg.version).option("--debug", "Write redacted API diagnostics to a private JSONL file").option("--output <format>", "Default output format for read commands").option("--quiet", "Suppress status and decorative output (data and errors still print)");
|
|
13695
14062
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
13696
14063
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
13697
14064
|
setQuiet(Boolean(root.quiet));
|
|
14065
|
+
let ancestor = actionCommand;
|
|
14066
|
+
let agentGuard = false;
|
|
14067
|
+
let dlp = false;
|
|
14068
|
+
while (ancestor) {
|
|
14069
|
+
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
14070
|
+
if (ancestor.name() === "dlp" && ancestor.parent?.name() === "runtime") dlp = true;
|
|
14071
|
+
ancestor = ancestor.parent;
|
|
14072
|
+
}
|
|
14073
|
+
if (dlp) {
|
|
14074
|
+
dlpDebugBodyEnvironment = { previous: process.env.PANW_AI_SEC_DEBUG_BODY };
|
|
14075
|
+
process.env.PANW_AI_SEC_DEBUG_BODY = "0";
|
|
14076
|
+
}
|
|
13698
14077
|
const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
|
|
13699
14078
|
if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
13700
14079
|
usageError(
|
|
@@ -13721,13 +14100,9 @@ function buildProgram() {
|
|
|
13721
14100
|
`debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
|
|
13722
14101
|
);
|
|
13723
14102
|
try {
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
13728
|
-
ancestor = ancestor.parent;
|
|
13729
|
-
}
|
|
13730
|
-
installDebugLogger(logPath, { omitBodies: agentGuard });
|
|
14103
|
+
installDebugLogger(logPath, {
|
|
14104
|
+
omitBodies: agentGuard || dlp
|
|
14105
|
+
});
|
|
13731
14106
|
} catch {
|
|
13732
14107
|
ui.error(
|
|
13733
14108
|
"Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs-cli",
|
|
3
3
|
"packageManager": "pnpm@10.6.5",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.10.0",
|
|
5
5
|
"description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"license": "MIT",
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@cdot65/prisma-airs-sdk": "0.30.
|
|
62
|
+
"@cdot65/prisma-airs-sdk": "0.30.1",
|
|
63
63
|
"@inquirer/prompts": "^8.3.0",
|
|
64
64
|
"chalk": "^5.6.2",
|
|
65
65
|
"commander": "^14.0.3",
|