@cdot65/prisma-airs-cli 5.9.0 → 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 +416 -74
- 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)
|
|
@@ -9951,6 +10183,24 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
9951
10183
|
uniqueNames(backup.dictionaries, "dictionary");
|
|
9952
10184
|
uniqueNames(backup.patterns, "data pattern");
|
|
9953
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
|
+
}
|
|
9954
10204
|
const unknownMapped = Object.keys(patternMap2).filter(
|
|
9955
10205
|
(name) => !backup.patterns.some((p) => p.name === name)
|
|
9956
10206
|
);
|
|
@@ -10004,6 +10254,7 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10004
10254
|
if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
|
|
10005
10255
|
const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
|
|
10006
10256
|
if (!match) {
|
|
10257
|
+
dictionaryFile(metadata, keywords);
|
|
10007
10258
|
dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
|
|
10008
10259
|
continue;
|
|
10009
10260
|
}
|
|
@@ -10044,6 +10295,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10044
10295
|
else missingPredefined.push({ name: source.name, candidates });
|
|
10045
10296
|
continue;
|
|
10046
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}`);
|
|
10047
10300
|
patternPlans.push({
|
|
10048
10301
|
sourceId: source.id,
|
|
10049
10302
|
name: match2.name,
|
|
@@ -10056,6 +10309,8 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10056
10309
|
const target = patternMap2[source.name];
|
|
10057
10310
|
const match2 = matchByName(destination.patterns, target, retired, "data pattern");
|
|
10058
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}`);
|
|
10059
10314
|
patternPlans.push({
|
|
10060
10315
|
sourceId: source.id,
|
|
10061
10316
|
name: target,
|
|
@@ -10164,7 +10419,15 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10164
10419
|
const missing = /* @__PURE__ */ new Set();
|
|
10165
10420
|
for (const leaf of ruleLeaves(item.source)) {
|
|
10166
10421
|
const id = leaf.value.id;
|
|
10167
|
-
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);
|
|
10168
10431
|
}
|
|
10169
10432
|
if (missing.size) {
|
|
10170
10433
|
const skippable = [...missing].filter((id) => unresolvedById.has(id));
|
|
@@ -10185,20 +10448,39 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
|
|
|
10185
10448
|
}
|
|
10186
10449
|
return plan;
|
|
10187
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
|
+
}
|
|
10188
10455
|
function planBindings(plan) {
|
|
10189
10456
|
const bindings = /* @__PURE__ */ new Map();
|
|
10190
10457
|
for (const item of plan.dictionaries)
|
|
10191
|
-
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
|
+
});
|
|
10192
10466
|
for (const item of plan.patterns)
|
|
10193
10467
|
if (item.existing?.id)
|
|
10194
10468
|
bindings.set(item.sourceId, {
|
|
10195
10469
|
id: item.existing.id,
|
|
10196
10470
|
name: item.name,
|
|
10471
|
+
kind: "pattern",
|
|
10472
|
+
technique: item.existing.detection_config?.technique,
|
|
10197
10473
|
version: item.existing.version ?? void 0
|
|
10198
10474
|
});
|
|
10199
10475
|
return bindings;
|
|
10200
10476
|
}
|
|
10201
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");
|
|
10202
10484
|
const detectionRules = structuredClone(item.source.detection_rules ?? []);
|
|
10203
10485
|
const known = /* @__PURE__ */ new Set();
|
|
10204
10486
|
let deferred = false;
|
|
@@ -10207,6 +10489,8 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10207
10489
|
known.add(leaf.value);
|
|
10208
10490
|
const id = leaf.value.id;
|
|
10209
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");
|
|
10210
10494
|
const binding = bindings.get(String(id));
|
|
10211
10495
|
if (!binding) {
|
|
10212
10496
|
if (requireBindings)
|
|
@@ -10216,6 +10500,7 @@ function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
|
10216
10500
|
deferred = true;
|
|
10217
10501
|
continue;
|
|
10218
10502
|
}
|
|
10503
|
+
assertLeafBinding(leaf, binding);
|
|
10219
10504
|
leaf.value.id = binding.id;
|
|
10220
10505
|
if (leaf.value.name != null) leaf.value.name = binding.name;
|
|
10221
10506
|
if (leaf.value.version != null) {
|
|
@@ -10254,6 +10539,7 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10254
10539
|
patterns: [],
|
|
10255
10540
|
profiles: [],
|
|
10256
10541
|
serverAdded: [],
|
|
10542
|
+
unverifiedCreates: [],
|
|
10257
10543
|
unresolved: structuredClone(plan.unresolved ?? [])
|
|
10258
10544
|
};
|
|
10259
10545
|
const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
|
|
@@ -10306,21 +10592,28 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10306
10592
|
if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
|
|
10307
10593
|
const created = await api.dictionaries.create({
|
|
10308
10594
|
metadata: item.metadata,
|
|
10309
|
-
file:
|
|
10310
|
-
`,
|
|
10595
|
+
file: dictionaryFile(item.metadata, item.keywords),
|
|
10311
10596
|
includeKeywords: true
|
|
10312
10597
|
});
|
|
10313
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 });
|
|
10314
10600
|
const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
|
|
10315
10601
|
const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
|
|
10316
|
-
if (!echo.matches || readBack.name !== item.name)
|
|
10602
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name)
|
|
10317
10603
|
throw new Error(
|
|
10318
10604
|
`Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10319
10605
|
);
|
|
10320
10606
|
if (echo.serverAdded.length)
|
|
10321
10607
|
result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
|
|
10322
|
-
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
|
+
});
|
|
10323
10615
|
result.dictionaries.push({ name: item.name, action: "created", id: created.id });
|
|
10616
|
+
result.unverifiedCreates.pop();
|
|
10324
10617
|
emitItem(
|
|
10325
10618
|
"dictionaries",
|
|
10326
10619
|
item.name,
|
|
@@ -10348,9 +10641,10 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10348
10641
|
if (!item.body) throw new Error("Data pattern plan is incomplete");
|
|
10349
10642
|
const created = await api.patterns.create(item.body);
|
|
10350
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 });
|
|
10351
10645
|
const readBack = await api.patterns.get(created.id);
|
|
10352
10646
|
const echo = compareEcho(item.body, readBack);
|
|
10353
|
-
if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10647
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10354
10648
|
throw new Error(
|
|
10355
10649
|
`Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10356
10650
|
);
|
|
@@ -10362,9 +10656,12 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10362
10656
|
bindings.set(item.sourceId, {
|
|
10363
10657
|
id: created.id,
|
|
10364
10658
|
name: item.name,
|
|
10659
|
+
kind: "pattern",
|
|
10660
|
+
technique: readBack.detection_config?.technique,
|
|
10365
10661
|
version: readBack.version ?? void 0
|
|
10366
10662
|
});
|
|
10367
10663
|
result.patterns.push({ name: item.name, action: "created", id: created.id });
|
|
10664
|
+
result.unverifiedCreates.pop();
|
|
10368
10665
|
emitItem("patterns", item.name, "created", result.patterns.length, plan.patterns.length);
|
|
10369
10666
|
}
|
|
10370
10667
|
if (plan.profiles.length)
|
|
@@ -10401,9 +10698,10 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10401
10698
|
}
|
|
10402
10699
|
const created = await api.profiles.create(body);
|
|
10403
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 });
|
|
10404
10702
|
const readBack = await api.profiles.get(created.id);
|
|
10405
10703
|
const echo = compareEcho(body, readBack);
|
|
10406
|
-
if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10704
|
+
if (!echo.matches || readBack.id !== created.id || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10407
10705
|
throw new Error(
|
|
10408
10706
|
`Restored data profile did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
|
|
10409
10707
|
);
|
|
@@ -10413,6 +10711,7 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10413
10711
|
fields: echo.serverAdded
|
|
10414
10712
|
});
|
|
10415
10713
|
result.profiles.push({ name: item.name, action: "created", id: created.id });
|
|
10714
|
+
result.unverifiedCreates.pop();
|
|
10416
10715
|
emitItem("profiles", item.name, "created", result.profiles.length, plan.profiles.length);
|
|
10417
10716
|
}
|
|
10418
10717
|
result.complete = true;
|
|
@@ -10421,6 +10720,9 @@ async function restoreDlpResources(api, plan, options = {}) {
|
|
|
10421
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(
|
|
10422
10721
|
error.message
|
|
10423
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`;
|
|
10424
10726
|
}
|
|
10425
10727
|
return result;
|
|
10426
10728
|
}
|
|
@@ -10470,7 +10772,7 @@ function failure(error) {
|
|
|
10470
10772
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
10471
10773
|
if (error instanceof ZodError) {
|
|
10472
10774
|
ui.error("Invalid DLP backup/request schema; content is not logged.");
|
|
10473
|
-
} else if (error instanceof
|
|
10775
|
+
} else if (error instanceof AISecSDKException4) {
|
|
10474
10776
|
ui.error(
|
|
10475
10777
|
`DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10476
10778
|
);
|
|
@@ -10544,6 +10846,20 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
|
10544
10846
|
for (const entry of rows("unresolved"))
|
|
10545
10847
|
blocks.push(`${entry.kind} ${entry.name}: ${entry.reason}`);
|
|
10546
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
|
+
}
|
|
10547
10863
|
if (rows("serverAdded").length) {
|
|
10548
10864
|
blocks.push(heading("Verified server-added fields (source omitted these)"));
|
|
10549
10865
|
for (const item of rows("serverAdded"))
|
|
@@ -10728,6 +11044,7 @@ function register6(dlp) {
|
|
|
10728
11044
|
{ key: "profiles", label: "Data profiles" },
|
|
10729
11045
|
{ key: "unresolved", label: "Unresolved references" },
|
|
10730
11046
|
{ key: "serverAdded", label: "Verified server-added fields" },
|
|
11047
|
+
{ key: "unverifiedCreates", label: "Created resources awaiting verification" },
|
|
10731
11048
|
{ key: "dryRun", label: "Dry run" },
|
|
10732
11049
|
{ key: "complete", label: "Complete" }
|
|
10733
11050
|
];
|
|
@@ -10771,7 +11088,7 @@ function registerDlpCommands(runtime) {
|
|
|
10771
11088
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
10772
11089
|
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
10773
11090
|
import { extname as extname2, resolve as resolve6 } from "path";
|
|
10774
|
-
import { AISecSDKException as
|
|
11091
|
+
import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
10775
11092
|
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
10776
11093
|
import { ZodError as ZodError2 } from "zod";
|
|
10777
11094
|
|
|
@@ -11492,7 +11809,7 @@ function failure2(error) {
|
|
|
11492
11809
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
11493
11810
|
if (error instanceof ZodError2) {
|
|
11494
11811
|
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
11495
|
-
} else if (error instanceof
|
|
11812
|
+
} else if (error instanceof AISecSDKException5) {
|
|
11496
11813
|
ui.error(
|
|
11497
11814
|
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
11498
11815
|
);
|
|
@@ -13689,12 +14006,41 @@ function buildProgram() {
|
|
|
13689
14006
|
const here = dirname5(fileURLToPath(import.meta.url));
|
|
13690
14007
|
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
13691
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
|
+
};
|
|
13692
14026
|
program.name("airs").description(
|
|
13693
14027
|
"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", "
|
|
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)");
|
|
13695
14029
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
13696
14030
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
13697
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
|
+
}
|
|
13698
14044
|
const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
|
|
13699
14045
|
if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
13700
14046
|
usageError(
|
|
@@ -13721,13 +14067,9 @@ function buildProgram() {
|
|
|
13721
14067
|
`debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
|
|
13722
14068
|
);
|
|
13723
14069
|
try {
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
13728
|
-
ancestor = ancestor.parent;
|
|
13729
|
-
}
|
|
13730
|
-
installDebugLogger(logPath, { omitBodies: agentGuard });
|
|
14070
|
+
installDebugLogger(logPath, {
|
|
14071
|
+
omitBodies: agentGuard || dlp
|
|
14072
|
+
});
|
|
13731
14073
|
} catch {
|
|
13732
14074
|
ui.error(
|
|
13733
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.9.
|
|
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",
|