@cdot65/prisma-airs-cli 5.8.5 → 5.9.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 +474 -83
- 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 {
|
|
@@ -8870,13 +8871,52 @@ async function parseBody(opts) {
|
|
|
8870
8871
|
}
|
|
8871
8872
|
}
|
|
8872
8873
|
|
|
8874
|
+
// src/cli/commands/dlp/visibility.ts
|
|
8875
|
+
function predefinedFlag(cmd) {
|
|
8876
|
+
return cmd.option(
|
|
8877
|
+
"--include-predefined",
|
|
8878
|
+
"Include predefined (PANW-shipped) records; listings show only tenant-created records by default"
|
|
8879
|
+
);
|
|
8880
|
+
}
|
|
8881
|
+
function visibleRecords(records, includePredefined) {
|
|
8882
|
+
return includePredefined ? records : records.filter((record) => record.type !== "predefined");
|
|
8883
|
+
}
|
|
8884
|
+
|
|
8873
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
|
+
}
|
|
8874
8914
|
async function buildMetadata(opts) {
|
|
8875
8915
|
if (opts.metadataFile) {
|
|
8876
|
-
return
|
|
8916
|
+
return await readMetadata(opts.metadataFile);
|
|
8877
8917
|
}
|
|
8878
8918
|
if (!opts.name || !opts.category || !opts.region || !opts.file) {
|
|
8879
|
-
throw new
|
|
8919
|
+
throw new CliUsageError("--name, --category, --region, and --file are required");
|
|
8880
8920
|
}
|
|
8881
8921
|
return {
|
|
8882
8922
|
name: opts.name,
|
|
@@ -8894,6 +8934,7 @@ function register(dlp) {
|
|
|
8894
8934
|
"Starting offset \u2014 rounds down to a page boundary",
|
|
8895
8935
|
(v) => Number.parseInt(v, 10)
|
|
8896
8936
|
).option("--sort <field,dir>", "(repeatable)", (v, p = []) => [...p, v]).option("--keywords", "Include keyword list in response").option("--include-keywords", "Alias for --keywords").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml");
|
|
8937
|
+
predefinedFlag(listCmd);
|
|
8897
8938
|
registerPageAliases(listCmd, { sizeFlag: "--size", sizeKey: "size" });
|
|
8898
8939
|
listCmd.action(async (opts) => {
|
|
8899
8940
|
try {
|
|
@@ -8902,23 +8943,24 @@ function register(dlp) {
|
|
|
8902
8943
|
const svc = new SdkDictionariesService(await loadDlpClientOptions());
|
|
8903
8944
|
const params = {
|
|
8904
8945
|
size,
|
|
8905
|
-
sort: opts.sort,
|
|
8946
|
+
sort: opts.sort ?? ["name,asc"],
|
|
8906
8947
|
keywords: includeKeywords ? true : void 0
|
|
8907
8948
|
};
|
|
8908
|
-
const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) :
|
|
8949
|
+
const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : (await svc.list({ ...params, page })).content;
|
|
8950
|
+
const visible = visibleRecords(all, opts.includePredefined);
|
|
8909
8951
|
dlpDictionaries.renderList(
|
|
8910
|
-
|
|
8952
|
+
{ content: visible, totalElements: visible.length },
|
|
8911
8953
|
await resolveOutput(listCmd, opts)
|
|
8912
8954
|
);
|
|
8913
8955
|
} catch (err) {
|
|
8914
8956
|
fail(err);
|
|
8915
8957
|
}
|
|
8916
8958
|
});
|
|
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) => {
|
|
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) => {
|
|
8918
8960
|
try {
|
|
8919
8961
|
const format = await resolveOutput(command, opts);
|
|
8920
8962
|
const metadata = await buildMetadata(opts);
|
|
8921
|
-
if (!opts.file) throw new
|
|
8963
|
+
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
8922
8964
|
const file = await readFile8(opts.file);
|
|
8923
8965
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
|
|
8924
8966
|
metadata,
|
|
@@ -8927,7 +8969,7 @@ function register(dlp) {
|
|
|
8927
8969
|
});
|
|
8928
8970
|
dlpDictionaries.renderCreated(r, format);
|
|
8929
8971
|
} catch (err) {
|
|
8930
|
-
|
|
8972
|
+
uploadFailure(err);
|
|
8931
8973
|
}
|
|
8932
8974
|
});
|
|
8933
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) => {
|
|
@@ -8945,11 +8987,11 @@ function register(dlp) {
|
|
|
8945
8987
|
});
|
|
8946
8988
|
group.command("replace <id>").description(
|
|
8947
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).'
|
|
8948
|
-
).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) => {
|
|
8949
8991
|
try {
|
|
8950
8992
|
const metadata = await buildMetadata(opts);
|
|
8951
8993
|
const format = await resolveOutput(command, opts);
|
|
8952
|
-
if (!opts.file) throw new
|
|
8994
|
+
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
8953
8995
|
const file = await readFile8(opts.file);
|
|
8954
8996
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
|
|
8955
8997
|
metadata,
|
|
@@ -8962,23 +9004,34 @@ function register(dlp) {
|
|
|
8962
9004
|
dlpDictionaries.renderReplaced(r, format);
|
|
8963
9005
|
}
|
|
8964
9006
|
} catch (err) {
|
|
8965
|
-
|
|
9007
|
+
uploadFailure(err);
|
|
8966
9008
|
}
|
|
8967
9009
|
});
|
|
8968
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) => {
|
|
8969
9011
|
try {
|
|
8970
9012
|
const format = await resolveOutput(command, opts);
|
|
8971
9013
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
8972
|
-
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
|
+
}
|
|
8973
9027
|
}
|
|
8974
|
-
const body = opts.bodyFile ? await parseBody({ bodyFile: opts.bodyFile }) : buildMergePatch({ set: opts.set, clear: opts.clear });
|
|
8975
9028
|
dlpDictionaries.renderPatched(
|
|
8976
9029
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
8977
9030
|
await new SdkDictionariesService(await loadDlpClientOptions()).patch(id, body),
|
|
8978
9031
|
format
|
|
8979
9032
|
);
|
|
8980
9033
|
} catch (err) {
|
|
8981
|
-
|
|
9034
|
+
dictionaryFailure(err);
|
|
8982
9035
|
}
|
|
8983
9036
|
});
|
|
8984
9037
|
group.command("delete <id>").description("Delete a dictionary").action(async (id) => {
|
|
@@ -9095,31 +9148,70 @@ function buildFilteringProfileBody(opts) {
|
|
|
9095
9148
|
if (opts.fileType?.length) body.file_type = opts.fileType;
|
|
9096
9149
|
return body;
|
|
9097
9150
|
}
|
|
9098
|
-
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) {
|
|
9099
9168
|
if (!opts.name) throw new Error("--name is required");
|
|
9100
|
-
|
|
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);
|
|
9101
9180
|
const body = {
|
|
9102
9181
|
name: opts.name,
|
|
9103
|
-
profile_type:
|
|
9182
|
+
profile_type: "advanced"
|
|
9104
9183
|
};
|
|
9105
9184
|
if (opts.description != null) body.description = opts.description;
|
|
9106
9185
|
if (opts.granular != null) body.is_granular_data_profile = !!opts.granular;
|
|
9107
9186
|
if (opts.patternId?.length) {
|
|
9108
|
-
const
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
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
|
+
});
|
|
9112
9209
|
body.detection_rules = [
|
|
9113
9210
|
{
|
|
9114
9211
|
rule_type: "expression_tree",
|
|
9115
9212
|
expression_tree: {
|
|
9116
|
-
operator_type:
|
|
9117
|
-
|
|
9118
|
-
data_pattern_id: id,
|
|
9119
|
-
confidence_level: opts.confidence ?? "high",
|
|
9120
|
-
occurrence_operator_type: "any",
|
|
9121
|
-
occurrence_count: 1
|
|
9122
|
-
}))
|
|
9213
|
+
operator_type: (opts.combinator ?? "or").toLowerCase(),
|
|
9214
|
+
sub_expressions: leaves
|
|
9123
9215
|
}
|
|
9124
9216
|
}
|
|
9125
9217
|
];
|
|
@@ -9157,8 +9249,8 @@ function register2(dlp) {
|
|
|
9157
9249
|
try {
|
|
9158
9250
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9159
9251
|
const svc = new SdkDataFilteringProfilesService(await loadDlpClientOptions());
|
|
9160
|
-
const all = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
|
|
9161
|
-
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"] });
|
|
9162
9254
|
dlpFilteringProfiles.renderList(r, await resolveOutput(listCmd, opts));
|
|
9163
9255
|
} catch (err) {
|
|
9164
9256
|
fail(err);
|
|
@@ -9321,14 +9413,17 @@ async function resolveWriteBody(opts) {
|
|
|
9321
9413
|
}
|
|
9322
9414
|
function register4(dlp) {
|
|
9323
9415
|
const group = dlp.command("patterns").description("DLP data patterns (full CRUD)");
|
|
9324
|
-
const listCmd =
|
|
9416
|
+
const listCmd = predefinedFlag(
|
|
9417
|
+
listFlags2(group.command("list").description("List data patterns (tenant-created by default)"))
|
|
9418
|
+
);
|
|
9325
9419
|
listCmd.action(async (opts) => {
|
|
9326
9420
|
try {
|
|
9327
9421
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9328
9422
|
const svc = new SdkDataPatternsService(await loadDlpClientOptions());
|
|
9329
|
-
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) :
|
|
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;
|
|
9424
|
+
const visible = visibleRecords(result, opts.includePredefined);
|
|
9330
9425
|
dlpPatterns.renderList(
|
|
9331
|
-
|
|
9426
|
+
{ content: visible, totalElements: visible.length },
|
|
9332
9427
|
await resolveOutput(listCmd, opts)
|
|
9333
9428
|
);
|
|
9334
9429
|
} catch (err) {
|
|
@@ -9403,6 +9498,9 @@ function register4(dlp) {
|
|
|
9403
9498
|
});
|
|
9404
9499
|
}
|
|
9405
9500
|
|
|
9501
|
+
// src/cli/commands/dlp/profiles.ts
|
|
9502
|
+
import { AISecSDKException as AISecSDKException3, ErrorType as ErrorType4 } from "@cdot65/prisma-airs-sdk";
|
|
9503
|
+
|
|
9406
9504
|
// src/airs/dlp/data-profiles.ts
|
|
9407
9505
|
var SdkDataProfilesService = class {
|
|
9408
9506
|
client;
|
|
@@ -9440,7 +9538,7 @@ function listFlags3(cmd) {
|
|
|
9440
9538
|
return cmd;
|
|
9441
9539
|
}
|
|
9442
9540
|
function writeFlags2(cmd) {
|
|
9443
|
-
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(
|
|
9444
9542
|
"--pattern-id <id>",
|
|
9445
9543
|
"Data pattern ID to include (repeatable). Builds a simple expression_tree.",
|
|
9446
9544
|
repeatable
|
|
@@ -9452,29 +9550,91 @@ function writeFlags2(cmd) {
|
|
|
9452
9550
|
"Raw JSON body file (escape hatch; required for complex rule trees)"
|
|
9453
9551
|
).option("--output <fmt>", "Output format", "pretty");
|
|
9454
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
|
+
}
|
|
9455
9570
|
async function resolveWriteBody2(opts) {
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
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);
|
|
9462
9587
|
return body;
|
|
9463
9588
|
}
|
|
9464
|
-
|
|
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
|
+
}
|
|
9465
9622
|
}
|
|
9466
9623
|
function register5(dlp) {
|
|
9467
9624
|
const group = dlp.command("profiles").description(
|
|
9468
9625
|
"DLP data profiles. No supported DELETE; status-based retirement is not live-verified."
|
|
9469
9626
|
);
|
|
9470
|
-
const listCmd =
|
|
9627
|
+
const listCmd = predefinedFlag(
|
|
9628
|
+
listFlags3(group.command("list").description("List data profiles (tenant-created by default)"))
|
|
9629
|
+
);
|
|
9471
9630
|
listCmd.action(async (opts) => {
|
|
9472
9631
|
try {
|
|
9473
9632
|
const { page, size } = resolvePageParams(listCmd, opts);
|
|
9474
9633
|
const svc = new SdkDataProfilesService(await loadDlpClientOptions());
|
|
9475
|
-
const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) :
|
|
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;
|
|
9635
|
+
const visible = visibleRecords(result, opts.includePredefined);
|
|
9476
9636
|
dlpProfiles.renderList(
|
|
9477
|
-
|
|
9637
|
+
{ content: visible, totalElements: visible.length },
|
|
9478
9638
|
await resolveOutput(listCmd, opts)
|
|
9479
9639
|
);
|
|
9480
9640
|
} catch (err) {
|
|
@@ -9492,7 +9652,7 @@ function register5(dlp) {
|
|
|
9492
9652
|
format
|
|
9493
9653
|
);
|
|
9494
9654
|
} catch (err) {
|
|
9495
|
-
|
|
9655
|
+
profileFailure(err);
|
|
9496
9656
|
}
|
|
9497
9657
|
}
|
|
9498
9658
|
);
|
|
@@ -9517,7 +9677,7 @@ function register5(dlp) {
|
|
|
9517
9677
|
format
|
|
9518
9678
|
);
|
|
9519
9679
|
} catch (err) {
|
|
9520
|
-
|
|
9680
|
+
profileFailure(err);
|
|
9521
9681
|
}
|
|
9522
9682
|
}
|
|
9523
9683
|
);
|
|
@@ -9527,16 +9687,24 @@ function register5(dlp) {
|
|
|
9527
9687
|
try {
|
|
9528
9688
|
const format = await resolveOutput(command, opts);
|
|
9529
9689
|
if (opts.bodyFile && (opts.set || opts.clear)) {
|
|
9530
|
-
throw new
|
|
9690
|
+
throw new CliUsageError("--body-file is mutually exclusive with --set/--clear");
|
|
9531
9691
|
}
|
|
9532
|
-
|
|
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);
|
|
9533
9701
|
dlpProfiles.renderPatched(
|
|
9534
9702
|
// biome-ignore lint/suspicious/noExplicitAny: buildMergePatch returns Record<string, unknown>, cast for patch()
|
|
9535
9703
|
await new SdkDataProfilesService(await loadDlpClientOptions()).patch(id, body),
|
|
9536
9704
|
format
|
|
9537
9705
|
);
|
|
9538
9706
|
} catch (err) {
|
|
9539
|
-
|
|
9707
|
+
profileFailure(err);
|
|
9540
9708
|
}
|
|
9541
9709
|
});
|
|
9542
9710
|
group.command("delete <id>").description("Not supported \u2014 explains the cleanup limitation and exits 2").action((id) => {
|
|
@@ -9554,7 +9722,7 @@ function register5(dlp) {
|
|
|
9554
9722
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
9555
9723
|
import { readFile as readFile9, stat } from "fs/promises";
|
|
9556
9724
|
import { extname, resolve as resolve5 } from "path";
|
|
9557
|
-
import { AISecSDKException as
|
|
9725
|
+
import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9558
9726
|
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
9559
9727
|
import { ZodError } from "zod";
|
|
9560
9728
|
|
|
@@ -9613,13 +9781,79 @@ function assertName(name) {
|
|
|
9613
9781
|
async function springPages(fetch, maxPages3, label) {
|
|
9614
9782
|
pagesLimit(maxPages3);
|
|
9615
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
|
+
};
|
|
9616
9797
|
for (let page = 0; page < maxPages3; page++) {
|
|
9617
9798
|
const result = await fetch(page);
|
|
9618
|
-
|
|
9619
|
-
|
|
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)
|
|
9620
9807
|
throw new Error(`Inventory of ${label} exceeds the 10000-record safety limit`);
|
|
9621
|
-
|
|
9622
|
-
|
|
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
|
+
}
|
|
9623
9857
|
if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
|
|
9624
9858
|
}
|
|
9625
9859
|
throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
|
|
@@ -9627,17 +9861,17 @@ async function springPages(fetch, maxPages3, label) {
|
|
|
9627
9861
|
async function inventories(api, maxPages3) {
|
|
9628
9862
|
return {
|
|
9629
9863
|
dictionaries: await springPages(
|
|
9630
|
-
(page) => api.dictionaries.list({ page, size: 100, keywords: true }),
|
|
9864
|
+
(page) => api.dictionaries.list({ page, size: 100, sort: ["name,asc"], keywords: true }),
|
|
9631
9865
|
maxPages3,
|
|
9632
9866
|
"dictionaries"
|
|
9633
9867
|
),
|
|
9634
9868
|
patterns: await springPages(
|
|
9635
|
-
(page) => api.patterns.list({ page, size: 100 }),
|
|
9869
|
+
(page) => api.patterns.list({ page, size: 100, sort: ["name,asc"] }),
|
|
9636
9870
|
maxPages3,
|
|
9637
9871
|
"data patterns"
|
|
9638
9872
|
),
|
|
9639
9873
|
profiles: await springPages(
|
|
9640
|
-
(page) => api.profiles.list({ page, size: 100 }),
|
|
9874
|
+
(page) => api.profiles.list({ page, size: 100, sort: ["name,asc"] }),
|
|
9641
9875
|
maxPages3,
|
|
9642
9876
|
"data profiles"
|
|
9643
9877
|
)
|
|
@@ -9752,6 +9986,10 @@ async function backupDlpResources(api, tsgId, options = {}) {
|
|
|
9752
9986
|
if (RETIRED_PROFILE.has(profile.profile_status ?? "active")) continue;
|
|
9753
9987
|
const name = profile.name ?? "(unnamed)";
|
|
9754
9988
|
try {
|
|
9989
|
+
if (!profile.detection_rules?.length)
|
|
9990
|
+
throw new Error(
|
|
9991
|
+
"has no exported detection rules (the API does not expose basic data profile content)"
|
|
9992
|
+
);
|
|
9755
9993
|
for (const leaf of ruleLeaves(profile)) {
|
|
9756
9994
|
const id = leaf.value.id;
|
|
9757
9995
|
if (id == null || id === "") {
|
|
@@ -9763,12 +10001,16 @@ async function backupDlpResources(api, tsgId, options = {}) {
|
|
|
9763
10001
|
if (leaf.technique === "dictionary") {
|
|
9764
10002
|
const record = dictionaryById.get(id);
|
|
9765
10003
|
if (!record) throw new Error(`references an unknown dictionary: ${id}`);
|
|
9766
|
-
if (leaf.value.version != null)
|
|
10004
|
+
if (leaf.value.version != null && leaf.value.version !== 1)
|
|
9767
10005
|
throw new Error(`references a version-pinned dictionary: ${id}`);
|
|
9768
10006
|
addDictionary(record);
|
|
9769
10007
|
} else {
|
|
9770
10008
|
const record = patternById.get(id);
|
|
9771
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");
|
|
10012
|
+
if (RETIRED_PATTERN.has(record.status ?? "active"))
|
|
10013
|
+
throw new Error(`references a retired data pattern: ${record.name ?? id}`);
|
|
9772
10014
|
if (leaf.value.version != null && record.version !== leaf.value.version)
|
|
9773
10015
|
throw new Error(
|
|
9774
10016
|
`references data pattern version ${leaf.value.version}, but the catalog holds version ${record.version ?? "unknown"}: ${id}`
|
|
@@ -9822,6 +10064,10 @@ function compareEcho(request, readBack) {
|
|
|
9822
10064
|
differences.push(path3);
|
|
9823
10065
|
return;
|
|
9824
10066
|
}
|
|
10067
|
+
if (path3.endsWith(".supported_confidence_levels") && Array.isArray(a) && Array.isArray(b) && a.every((level) => b.includes(level))) {
|
|
10068
|
+
if (a.length !== b.length) serverAdded.push(path3);
|
|
10069
|
+
return;
|
|
10070
|
+
}
|
|
9825
10071
|
if (Array.isArray(a) || Array.isArray(b)) {
|
|
9826
10072
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
9827
10073
|
differences.push(path3);
|
|
@@ -9849,6 +10095,20 @@ function keywordSet(value) {
|
|
|
9849
10095
|
return void 0;
|
|
9850
10096
|
return [...value].sort();
|
|
9851
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
|
+
}
|
|
9852
10112
|
function dictionaryEcho(metadata, record, keywords) {
|
|
9853
10113
|
const { original_file_name, ...projection } = metadata;
|
|
9854
10114
|
const echo = compareEcho(projection, record);
|
|
@@ -9864,7 +10124,8 @@ function dictionaryRequest(source, name) {
|
|
|
9864
10124
|
original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
|
|
9865
10125
|
region_name: source.region_name,
|
|
9866
10126
|
...source.description != null ? { description: source.description } : {},
|
|
9867
|
-
...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) } : {}
|
|
9868
10129
|
};
|
|
9869
10130
|
const parsed = DictionaryRequestSchema.safeParse(metadata);
|
|
9870
10131
|
if (!parsed.success)
|
|
@@ -9922,6 +10183,24 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
9922
10183
|
uniqueNames(backup.dictionaries, "dictionary");
|
|
9923
10184
|
uniqueNames(backup.patterns, "data pattern");
|
|
9924
10185
|
uniqueNames(backup.profiles, "data profile");
|
|
10186
|
+
const sourceBindings = /* @__PURE__ */ new Map();
|
|
10187
|
+
for (const [kind, records] of [
|
|
10188
|
+
["dictionary", backup.dictionaries],
|
|
10189
|
+
["pattern", backup.patterns]
|
|
10190
|
+
]) {
|
|
10191
|
+
for (const record of records) {
|
|
10192
|
+
if (!record.id) continue;
|
|
10193
|
+
if (sourceBindings.has(record.id))
|
|
10194
|
+
throw new Error("Backup contains duplicate dependency identities");
|
|
10195
|
+
sourceBindings.set(record.id, {
|
|
10196
|
+
id: record.id,
|
|
10197
|
+
name: record.name ?? "",
|
|
10198
|
+
kind,
|
|
10199
|
+
technique: kind === "dictionary" ? "dictionary" : record.detection_config?.technique,
|
|
10200
|
+
version: kind === "dictionary" ? 1 : record.version ?? void 0
|
|
10201
|
+
});
|
|
10202
|
+
}
|
|
10203
|
+
}
|
|
9925
10204
|
const unknownMapped = Object.keys(patternMap2).filter(
|
|
9926
10205
|
(name) => !backup.patterns.some((p) => p.name === name)
|
|
9927
10206
|
);
|
|
@@ -9975,6 +10254,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
9975
10254
|
if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
|
|
9976
10255
|
const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
|
|
9977
10256
|
if (!match) {
|
|
10257
|
+
dictionaryFile(metadata, keywords);
|
|
9978
10258
|
dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
|
|
9979
10259
|
continue;
|
|
9980
10260
|
}
|
|
@@ -10015,6 +10295,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10015
10295
|
else missingPredefined.push({ name: source.name, candidates });
|
|
10016
10296
|
continue;
|
|
10017
10297
|
}
|
|
10298
|
+
if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
|
|
10299
|
+
throw new Error(`Destination data pattern detection technique differs: ${source.name}`);
|
|
10018
10300
|
patternPlans.push({
|
|
10019
10301
|
sourceId: source.id,
|
|
10020
10302
|
name: match2.name,
|
|
@@ -10027,6 +10309,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10027
10309
|
const target = patternMap2[source.name];
|
|
10028
10310
|
const match2 = matchByName(destination.patterns, target, retired, "data pattern");
|
|
10029
10311
|
if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
|
|
10312
|
+
if (!source.detection_config?.technique || match2.detection_config?.technique !== source.detection_config.technique)
|
|
10313
|
+
throw new Error(`Mapped destination data pattern detection technique differs: ${target}`);
|
|
10030
10314
|
patternPlans.push({
|
|
10031
10315
|
sourceId: source.id,
|
|
10032
10316
|
name: target,
|
|
@@ -10049,6 +10333,20 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10049
10333
|
`Data pattern uses a tenant-bound detection technique: ${source.name}; provision it in the destination and bind it with --pattern-map "${source.name}=<destination-name>"`
|
|
10050
10334
|
);
|
|
10051
10335
|
}
|
|
10336
|
+
if (["regex", "weighted_regex"].includes(source.detection_config?.technique ?? "") && !(Array.isArray(source.matching_rules?.regexes) && source.matching_rules.regexes.length)) {
|
|
10337
|
+
if (options.skipUnresolved) {
|
|
10338
|
+
unresolved.push({
|
|
10339
|
+
kind: "pattern",
|
|
10340
|
+
sourceId: source.id,
|
|
10341
|
+
name: source.name,
|
|
10342
|
+
reason: "detection content is not exported by the API (copy of a predefined pattern)"
|
|
10343
|
+
});
|
|
10344
|
+
continue;
|
|
10345
|
+
}
|
|
10346
|
+
throw new Error(
|
|
10347
|
+
`Data pattern has no exported detection content (a copy of a predefined pattern): ${source.name}; bind an equivalent with --pattern-map "${source.name}=<destination-name>" or exclude dependents with --skip-unresolved`
|
|
10348
|
+
);
|
|
10349
|
+
}
|
|
10052
10350
|
const name = `${prefix}${source.name}`;
|
|
10053
10351
|
assertName(name);
|
|
10054
10352
|
const body = patternRequest(source, name);
|
|
@@ -10121,7 +10419,15 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10121
10419
|
const missing = /* @__PURE__ */ new Set();
|
|
10122
10420
|
for (const leaf of ruleLeaves(item.source)) {
|
|
10123
10421
|
const id = leaf.value.id;
|
|
10124
|
-
if (id
|
|
10422
|
+
if (id == null || id === "") continue;
|
|
10423
|
+
if (typeof id !== "string") throw new Error("Backup contains a non-string rule reference ID");
|
|
10424
|
+
const sourceBinding = sourceBindings.get(id);
|
|
10425
|
+
if (sourceBinding) {
|
|
10426
|
+
assertLeafBinding(leaf, sourceBinding);
|
|
10427
|
+
if (leaf.value.version != null && leaf.value.version !== sourceBinding.version)
|
|
10428
|
+
throw new Error("Backup rule pins an unknown source dependency version");
|
|
10429
|
+
}
|
|
10430
|
+
if (!planned.has(id)) missing.add(id);
|
|
10125
10431
|
}
|
|
10126
10432
|
if (missing.size) {
|
|
10127
10433
|
const skippable = [...missing].filter((id) => unresolvedById.has(id));
|
|
@@ -10142,20 +10448,39 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10142
10448
|
}
|
|
10143
10449
|
return plan;
|
|
10144
10450
|
}
|
|
10451
|
+
function assertLeafBinding(leaf, binding) {
|
|
10452
|
+
if (leaf.technique === "dictionary" !== (binding.kind === "dictionary") || leaf.technique !== binding.technique)
|
|
10453
|
+
throw new Error("Data profile rule detection technique does not match its dependency");
|
|
10454
|
+
}
|
|
10145
10455
|
function planBindings(plan) {
|
|
10146
10456
|
const bindings = /* @__PURE__ */ new Map();
|
|
10147
10457
|
for (const item of plan.dictionaries)
|
|
10148
|
-
if (item.existing?.id)
|
|
10458
|
+
if (item.existing?.id)
|
|
10459
|
+
bindings.set(item.sourceId, {
|
|
10460
|
+
id: item.existing.id,
|
|
10461
|
+
name: item.name,
|
|
10462
|
+
version: 1,
|
|
10463
|
+
kind: "dictionary",
|
|
10464
|
+
technique: "dictionary"
|
|
10465
|
+
});
|
|
10149
10466
|
for (const item of plan.patterns)
|
|
10150
10467
|
if (item.existing?.id)
|
|
10151
10468
|
bindings.set(item.sourceId, {
|
|
10152
10469
|
id: item.existing.id,
|
|
10153
10470
|
name: item.name,
|
|
10471
|
+
kind: "pattern",
|
|
10472
|
+
technique: item.existing.detection_config?.technique,
|
|
10154
10473
|
version: item.existing.version ?? void 0
|
|
10155
10474
|
});
|
|
10156
10475
|
return bindings;
|
|
10157
10476
|
}
|
|
10158
10477
|
function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
10478
|
+
if (item.action === "create" && item.name.length > 32)
|
|
10479
|
+
throw new Error("Data profile name exceeds the live API limit of 32 characters");
|
|
10480
|
+
if (item.action === "create" && item.source.profile_type === "basic")
|
|
10481
|
+
throw new Error("Data profile basic writes cannot round-trip through the API");
|
|
10482
|
+
if (item.action === "create" && !item.source.detection_rules?.length)
|
|
10483
|
+
throw new Error("Data profile has no exported detection rules to create");
|
|
10159
10484
|
const detectionRules = structuredClone(item.source.detection_rules ?? []);
|
|
10160
10485
|
const known = /* @__PURE__ */ new Set();
|
|
10161
10486
|
let deferred = false;
|
|
@@ -10164,6 +10489,8 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10164
10489
|
known.add(leaf.value);
|
|
10165
10490
|
const id = leaf.value.id;
|
|
10166
10491
|
if (id == null || id === "") continue;
|
|
10492
|
+
if (leaf.technique === "dictionary" && leaf.value.version != null && leaf.value.version !== 1)
|
|
10493
|
+
throw new Error("Dictionary rule has an unsupported version pin");
|
|
10167
10494
|
const binding = bindings.get(String(id));
|
|
10168
10495
|
if (!binding) {
|
|
10169
10496
|
if (requireBindings)
|
|
@@ -10173,6 +10500,7 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10173
10500
|
deferred = true;
|
|
10174
10501
|
continue;
|
|
10175
10502
|
}
|
|
10503
|
+
assertLeafBinding(leaf, binding);
|
|
10176
10504
|
leaf.value.id = binding.id;
|
|
10177
10505
|
if (leaf.value.name != null) leaf.value.name = binding.name;
|
|
10178
10506
|
if (leaf.value.version != null) {
|
|
@@ -10211,6 +10539,7 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10211
10539
|
patterns: [],
|
|
10212
10540
|
profiles: [],
|
|
10213
10541
|
serverAdded: [],
|
|
10542
|
+
unverifiedCreates: [],
|
|
10214
10543
|
unresolved: structuredClone(plan.unresolved ?? [])
|
|
10215
10544
|
};
|
|
10216
10545
|
const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
|
|
@@ -10263,19 +10592,28 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10263
10592
|
if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
|
|
10264
10593
|
const created = await api.dictionaries.create({
|
|
10265
10594
|
metadata: item.metadata,
|
|
10266
|
-
file:
|
|
10267
|
-
`,
|
|
10595
|
+
file: dictionaryFile(item.metadata, item.keywords),
|
|
10268
10596
|
includeKeywords: true
|
|
10269
10597
|
});
|
|
10270
10598
|
if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10599
|
+
result.unverifiedCreates.push({ kind: "dictionaries", name: item.name, id: created.id });
|
|
10271
10600
|
const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
|
|
10272
10601
|
const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
|
|
10273
|
-
if (!echo.matches || readBack.name !== item.name)
|
|
10274
|
-
throw new Error(
|
|
10602
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name)
|
|
10603
|
+
throw new Error(
|
|
10604
|
+
`Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10605
|
+
);
|
|
10275
10606
|
if (echo.serverAdded.length)
|
|
10276
10607
|
result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
|
|
10277
|
-
bindings.set(item.sourceId, {
|
|
10608
|
+
bindings.set(item.sourceId, {
|
|
10609
|
+
id: created.id,
|
|
10610
|
+
name: item.name,
|
|
10611
|
+
version: 1,
|
|
10612
|
+
kind: "dictionary",
|
|
10613
|
+
technique: "dictionary"
|
|
10614
|
+
});
|
|
10278
10615
|
result.dictionaries.push({ name: item.name, action: "created", id: created.id });
|
|
10616
|
+
result.unverifiedCreates.pop();
|
|
10279
10617
|
emitItem(
|
|
10280
10618
|
"dictionaries",
|
|
10281
10619
|
item.name,
|
|
@@ -10303,10 +10641,13 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10303
10641
|
if (!item.body) throw new Error("Data pattern plan is incomplete");
|
|
10304
10642
|
const created = await api.patterns.create(item.body);
|
|
10305
10643
|
if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10644
|
+
result.unverifiedCreates.push({ kind: "patterns", name: item.name, id: created.id });
|
|
10306
10645
|
const readBack = await api.patterns.get(created.id);
|
|
10307
10646
|
const echo = compareEcho(item.body, readBack);
|
|
10308
|
-
if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10309
|
-
throw new Error(
|
|
10647
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10648
|
+
throw new Error(
|
|
10649
|
+
`Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10650
|
+
);
|
|
10310
10651
|
if (echo.serverAdded.length)
|
|
10311
10652
|
result.serverAdded.push({
|
|
10312
10653
|
resource: `data pattern ${item.name}`,
|
|
@@ -10315,9 +10656,12 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10315
10656
|
bindings.set(item.sourceId, {
|
|
10316
10657
|
id: created.id,
|
|
10317
10658
|
name: item.name,
|
|
10659
|
+
kind: "pattern",
|
|
10660
|
+
technique: readBack.detection_config?.technique,
|
|
10318
10661
|
version: readBack.version ?? void 0
|
|
10319
10662
|
});
|
|
10320
10663
|
result.patterns.push({ name: item.name, action: "created", id: created.id });
|
|
10664
|
+
result.unverifiedCreates.pop();
|
|
10321
10665
|
emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
|
|
10322
10666
|
}
|
|
10323
10667
|
if (plan.profiles.length)
|
|
@@ -10354,16 +10698,20 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10354
10698
|
}
|
|
10355
10699
|
const created = await api.profiles.create(body);
|
|
10356
10700
|
if (!created.id) throw new Error(`Restored data profile did not verify: ${item.name}`);
|
|
10701
|
+
result.unverifiedCreates.push({ kind: "profiles", name: item.name, id: created.id });
|
|
10357
10702
|
const readBack = await api.profiles.get(created.id);
|
|
10358
10703
|
const echo = compareEcho(body, readBack);
|
|
10359
|
-
if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10360
|
-
throw new Error(
|
|
10704
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10705
|
+
throw new Error(
|
|
10706
|
+
`Restored data profile did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10707
|
+
);
|
|
10361
10708
|
if (echo.serverAdded.length)
|
|
10362
10709
|
result.serverAdded.push({
|
|
10363
10710
|
resource: `data profile ${item.name}`,
|
|
10364
10711
|
fields: echo.serverAdded
|
|
10365
10712
|
});
|
|
10366
10713
|
result.profiles.push({ name: item.name, action: "created", id: created.id });
|
|
10714
|
+
result.unverifiedCreates.pop();
|
|
10367
10715
|
emitItem("profiles", item.name, "created", result.profiles.length, plan.profiles.length);
|
|
10368
10716
|
}
|
|
10369
10717
|
result.complete = true;
|
|
@@ -10372,6 +10720,9 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10372
10720
|
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(
|
|
10373
10721
|
error.message
|
|
10374
10722
|
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
10723
|
+
const unverified = result.unverifiedCreates[0];
|
|
10724
|
+
if (unverified)
|
|
10725
|
+
result.error += `; unverified create: ${unverified.kind} ${unverified.name} (id ${unverified.id}); inspect this resource before retrying`;
|
|
10375
10726
|
}
|
|
10376
10727
|
return result;
|
|
10377
10728
|
}
|
|
@@ -10421,7 +10772,7 @@ function failure(error) {
|
|
|
10421
10772
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
10422
10773
|
if (error instanceof ZodError) {
|
|
10423
10774
|
ui.error("Invalid DLP backup/request schema; content is not logged.");
|
|
10424
|
-
} else if (error instanceof
|
|
10775
|
+
} else if (error instanceof AISecSDKException4) {
|
|
10425
10776
|
ui.error(
|
|
10426
10777
|
`DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10427
10778
|
);
|
|
@@ -10495,6 +10846,20 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
|
10495
10846
|
for (const entry of rows("unresolved"))
|
|
10496
10847
|
blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
|
|
10497
10848
|
}
|
|
10849
|
+
if (rows("unverifiedCreates").length) {
|
|
10850
|
+
blocks.push(heading("Created resources awaiting verification"));
|
|
10851
|
+
blocks.push(
|
|
10852
|
+
formatOutput(
|
|
10853
|
+
rows("unverifiedCreates"),
|
|
10854
|
+
[
|
|
10855
|
+
{ key: "kind", label: "Resource" },
|
|
10856
|
+
{ key: "name", label: "Name" },
|
|
10857
|
+
{ key: "id", label: "Created ID" }
|
|
10858
|
+
],
|
|
10859
|
+
markdown ? "markdown" : "table"
|
|
10860
|
+
)
|
|
10861
|
+
);
|
|
10862
|
+
}
|
|
10498
10863
|
if (rows("serverAdded").length) {
|
|
10499
10864
|
blocks.push(heading("Verified server-added fields (source omitted these)"));
|
|
10500
10865
|
for (const item of rows("serverAdded"))
|
|
@@ -10679,6 +11044,7 @@ function register6(dlp) {
|
|
|
10679
11044
|
{ key: "profiles", label: "Data profiles" },
|
|
10680
11045
|
{ key: "unresolved", label: "Unresolved references" },
|
|
10681
11046
|
{ key: "serverAdded", label: "Verified server-added fields" },
|
|
11047
|
+
{ key: "unverifiedCreates", label: "Created resources awaiting verification" },
|
|
10682
11048
|
{ key: "dryRun", label: "Dry run" },
|
|
10683
11049
|
{ key: "complete", label: "Complete" }
|
|
10684
11050
|
];
|
|
@@ -10722,7 +11088,7 @@ function registerDlpCommands(runtime) {
|
|
|
10722
11088
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
10723
11089
|
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
10724
11090
|
import { extname as extname2, resolve as resolve6 } from "path";
|
|
10725
|
-
import { AISecSDKException as
|
|
11091
|
+
import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
10726
11092
|
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
10727
11093
|
import { ZodError as ZodError2 } from "zod";
|
|
10728
11094
|
|
|
@@ -11443,7 +11809,7 @@ function failure2(error) {
|
|
|
11443
11809
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
11444
11810
|
if (error instanceof ZodError2) {
|
|
11445
11811
|
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
11446
|
-
} else if (error instanceof
|
|
11812
|
+
} else if (error instanceof AISecSDKException5) {
|
|
11447
11813
|
ui.error(
|
|
11448
11814
|
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
11449
11815
|
);
|
|
@@ -13640,12 +14006,41 @@ function buildProgram() {
|
|
|
13640
14006
|
const here = dirname5(fileURLToPath(import.meta.url));
|
|
13641
14007
|
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
13642
14008
|
const program = new Command();
|
|
14009
|
+
let dlpDebugBodyEnvironment;
|
|
14010
|
+
const restoreDlpDebugBody = () => {
|
|
14011
|
+
if (!dlpDebugBodyEnvironment) return;
|
|
14012
|
+
const { previous } = dlpDebugBodyEnvironment;
|
|
14013
|
+
if (previous === void 0) delete process.env.PANW_AI_SEC_DEBUG_BODY;
|
|
14014
|
+
else process.env.PANW_AI_SEC_DEBUG_BODY = previous;
|
|
14015
|
+
dlpDebugBodyEnvironment = void 0;
|
|
14016
|
+
};
|
|
14017
|
+
program.hook("postAction", restoreDlpDebugBody);
|
|
14018
|
+
const parseAsync = program.parseAsync.bind(program);
|
|
14019
|
+
program.parseAsync = async (...args) => {
|
|
14020
|
+
try {
|
|
14021
|
+
return await parseAsync(...args);
|
|
14022
|
+
} finally {
|
|
14023
|
+
restoreDlpDebugBody();
|
|
14024
|
+
}
|
|
14025
|
+
};
|
|
13643
14026
|
program.name("airs").description(
|
|
13644
14027
|
"CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
|
|
13645
|
-
).version(pkg.version).option("--debug", "
|
|
14028
|
+
).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)");
|
|
13646
14029
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
13647
14030
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
13648
14031
|
setQuiet(Boolean(root.quiet));
|
|
14032
|
+
let ancestor = actionCommand;
|
|
14033
|
+
let agentGuard = false;
|
|
14034
|
+
let dlp = false;
|
|
14035
|
+
while (ancestor) {
|
|
14036
|
+
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
14037
|
+
if (ancestor.name() === "dlp" && ancestor.parent?.name() === "runtime") dlp = true;
|
|
14038
|
+
ancestor = ancestor.parent;
|
|
14039
|
+
}
|
|
14040
|
+
if (dlp) {
|
|
14041
|
+
dlpDebugBodyEnvironment = { previous: process.env.PANW_AI_SEC_DEBUG_BODY };
|
|
14042
|
+
process.env.PANW_AI_SEC_DEBUG_BODY = "0";
|
|
14043
|
+
}
|
|
13649
14044
|
const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
|
|
13650
14045
|
if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
13651
14046
|
usageError(
|
|
@@ -13672,13 +14067,9 @@ function buildProgram() {
|
|
|
13672
14067
|
`debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
|
|
13673
14068
|
);
|
|
13674
14069
|
try {
|
|
13675
|
-
|
|
13676
|
-
|
|
13677
|
-
|
|
13678
|
-
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
13679
|
-
ancestor = ancestor.parent;
|
|
13680
|
-
}
|
|
13681
|
-
installDebugLogger(logPath, { omitBodies: agentGuard });
|
|
14070
|
+
installDebugLogger(logPath, {
|
|
14071
|
+
omitBodies: agentGuard || dlp
|
|
14072
|
+
});
|
|
13682
14073
|
} catch {
|
|
13683
14074
|
ui.error(
|
|
13684
14075
|
"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.9.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.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",
|