@cdot65/prisma-airs-cli 5.7.1 → 5.8.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 +1137 -143
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -59,7 +59,7 @@ function installProcessGuards() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// src/cli/program.ts
|
|
62
|
-
import { randomUUID as
|
|
62
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
63
63
|
import { readFileSync as readFileSync4 } from "fs";
|
|
64
64
|
import { dirname as dirname5, join as join5 } from "path";
|
|
65
65
|
import { fileURLToPath } from "url";
|
|
@@ -142,8 +142,8 @@ function modelSecurityClientOptions(config) {
|
|
|
142
142
|
|
|
143
143
|
// src/reports/agentguard.ts
|
|
144
144
|
async function collectAgentGuardReport(client, options = {}) {
|
|
145
|
-
const
|
|
146
|
-
if (!Number.isSafeInteger(
|
|
145
|
+
const maxPages3 = options.maxPages ?? 40;
|
|
146
|
+
if (!Number.isSafeInteger(maxPages3) || maxPages3 < 1 || maxPages3 > 100)
|
|
147
147
|
throw new Error("maxPages must be between 1 and 100");
|
|
148
148
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
149
149
|
const end = now();
|
|
@@ -205,7 +205,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
205
205
|
const seen = /* @__PURE__ */ new Set();
|
|
206
206
|
let total;
|
|
207
207
|
try {
|
|
208
|
-
for (let page = 0; page <
|
|
208
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
209
209
|
const r = await read(rows.length);
|
|
210
210
|
s.pages++;
|
|
211
211
|
if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
|
|
@@ -222,7 +222,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
222
222
|
if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
|
|
223
223
|
if (!r.rows.length) throw new Error();
|
|
224
224
|
}
|
|
225
|
-
s.notes.push(`Reached the ${
|
|
225
|
+
s.notes.push(`Reached the ${maxPages3}-page budget.`);
|
|
226
226
|
throw new Error();
|
|
227
227
|
} catch (error) {
|
|
228
228
|
if (!rows.length) throw error;
|
|
@@ -3494,7 +3494,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3494
3494
|
const outputError = (error) => controller.abort(error);
|
|
3495
3495
|
let stream;
|
|
3496
3496
|
let failed = false;
|
|
3497
|
-
let
|
|
3497
|
+
let failure3;
|
|
3498
3498
|
try {
|
|
3499
3499
|
const format = await resolveOutput(command, opts, { allowed: ["pretty", "json", "yaml"] });
|
|
3500
3500
|
const config = await loadConfig({ aiGwInferenceEndpoint: opts.endpoint });
|
|
@@ -3521,8 +3521,8 @@ function registerAiGatewayInference(parent) {
|
|
|
3521
3521
|
process.once("SIGINT", interrupt);
|
|
3522
3522
|
process.once("SIGTERM", interrupt);
|
|
3523
3523
|
process.stdout.on("error", outputError);
|
|
3524
|
-
const write = (text2) => new Promise((
|
|
3525
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3524
|
+
const write = (text2) => new Promise((resolve9, reject) => {
|
|
3525
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve9());
|
|
3526
3526
|
});
|
|
3527
3527
|
const requestOptions = { signal: controller.signal };
|
|
3528
3528
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -3557,7 +3557,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3557
3557
|
}
|
|
3558
3558
|
} catch (error) {
|
|
3559
3559
|
failed = true;
|
|
3560
|
-
|
|
3560
|
+
failure3 = error;
|
|
3561
3561
|
} finally {
|
|
3562
3562
|
await stream?.cancel();
|
|
3563
3563
|
controller.abort();
|
|
@@ -3565,10 +3565,10 @@ function registerAiGatewayInference(parent) {
|
|
|
3565
3565
|
process.removeListener("SIGTERM", interrupt);
|
|
3566
3566
|
process.stdout.removeListener("error", outputError);
|
|
3567
3567
|
}
|
|
3568
|
-
if (failed &&
|
|
3569
|
-
if (
|
|
3570
|
-
usageError(
|
|
3571
|
-
fail(
|
|
3568
|
+
if (failed && failure3?.code !== "EPIPE") {
|
|
3569
|
+
if (failure3?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
|
|
3570
|
+
usageError(failure3 instanceof Error ? failure3.message : "Invalid inference options");
|
|
3571
|
+
fail(failure3);
|
|
3572
3572
|
}
|
|
3573
3573
|
});
|
|
3574
3574
|
}
|
|
@@ -3896,20 +3896,20 @@ function renderGatewayDetail(value, format) {
|
|
|
3896
3896
|
);
|
|
3897
3897
|
if (rendered) console.log(rendered);
|
|
3898
3898
|
}
|
|
3899
|
-
async function runList(command, opts, label,
|
|
3899
|
+
async function runList(command, opts, label, load4) {
|
|
3900
3900
|
try {
|
|
3901
3901
|
const format = await resolveOutput(command, opts);
|
|
3902
3902
|
const client = await createAiGatewayClient();
|
|
3903
|
-
renderGatewayList(responseItems(await
|
|
3903
|
+
renderGatewayList(responseItems(await load4(client)), format, label);
|
|
3904
3904
|
} catch (error) {
|
|
3905
3905
|
failAiGateway(error);
|
|
3906
3906
|
}
|
|
3907
3907
|
}
|
|
3908
|
-
async function runDetail(command, opts,
|
|
3908
|
+
async function runDetail(command, opts, load4) {
|
|
3909
3909
|
try {
|
|
3910
3910
|
const format = await resolveOutput(command, opts);
|
|
3911
3911
|
const client = await createAiGatewayClient();
|
|
3912
|
-
renderGatewayDetail(await
|
|
3912
|
+
renderGatewayDetail(await load4(client), format);
|
|
3913
3913
|
} catch (error) {
|
|
3914
3914
|
failAiGateway(error);
|
|
3915
3915
|
}
|
|
@@ -5244,8 +5244,8 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5244
5244
|
const iso = z2.string().datetime({ offset: true });
|
|
5245
5245
|
if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
|
|
5246
5246
|
usageError("Provide a valid increasing ISO-8601 window with timezone");
|
|
5247
|
-
const
|
|
5248
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
5247
|
+
const maxPages3 = Number(opts.maxPages);
|
|
5248
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
|
|
5249
5249
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
5250
5250
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
5251
5251
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -5269,7 +5269,7 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5269
5269
|
ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
|
|
5270
5270
|
const report = await collectGatewayEnvironmentReport(client, {
|
|
5271
5271
|
title: opts.title,
|
|
5272
|
-
maxPages:
|
|
5272
|
+
maxPages: maxPages3,
|
|
5273
5273
|
workspace: opts.workspace,
|
|
5274
5274
|
tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
|
|
5275
5275
|
start,
|
|
@@ -5900,8 +5900,8 @@ async function withTimeout(promise, ms) {
|
|
|
5900
5900
|
try {
|
|
5901
5901
|
return await Promise.race([
|
|
5902
5902
|
promise,
|
|
5903
|
-
new Promise((
|
|
5904
|
-
timer = setTimeout(() =>
|
|
5903
|
+
new Promise((resolve9) => {
|
|
5904
|
+
timer = setTimeout(() => resolve9(TIMED_OUT), ms);
|
|
5905
5905
|
})
|
|
5906
5906
|
]);
|
|
5907
5907
|
} finally {
|
|
@@ -6186,12 +6186,12 @@ function hasBin(bin) {
|
|
|
6186
6186
|
}
|
|
6187
6187
|
}
|
|
6188
6188
|
function run2(bin, args, label) {
|
|
6189
|
-
return new Promise((
|
|
6189
|
+
return new Promise((resolve9, reject) => {
|
|
6190
6190
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
6191
6191
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
6192
6192
|
child.on("close", (code) => {
|
|
6193
6193
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
6194
|
-
else
|
|
6194
|
+
else resolve9();
|
|
6195
6195
|
});
|
|
6196
6196
|
});
|
|
6197
6197
|
}
|
|
@@ -6801,8 +6801,8 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6801
6801
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6802
6802
|
if (format !== "html" && format !== "markdown")
|
|
6803
6803
|
usageError("Report output must be html or markdown");
|
|
6804
|
-
const
|
|
6805
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
6804
|
+
const maxPages3 = Number(opts.maxPages);
|
|
6805
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
|
|
6806
6806
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
6807
6807
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
6808
6808
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -6826,7 +6826,7 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6826
6826
|
ui.status("Collecting Red Team statistics and current configuration (read-only)...");
|
|
6827
6827
|
const report = await collectRedTeamEnvironmentReport(client, {
|
|
6828
6828
|
title: opts.title,
|
|
6829
|
-
maxPages:
|
|
6829
|
+
maxPages: maxPages3
|
|
6830
6830
|
});
|
|
6831
6831
|
const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
|
|
6832
6832
|
if (outputPath) {
|
|
@@ -8085,9 +8085,9 @@ function registerRedteamCommand(program) {
|
|
|
8085
8085
|
}
|
|
8086
8086
|
|
|
8087
8087
|
// src/cli/commands/runtime.ts
|
|
8088
|
-
import { randomUUID as
|
|
8088
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
8089
8089
|
import * as fs5 from "fs";
|
|
8090
|
-
import { readFile as
|
|
8090
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
8091
8091
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
8092
8092
|
import chalk11 from "chalk";
|
|
8093
8093
|
|
|
@@ -9550,23 +9550,1017 @@ function register5(dlp) {
|
|
|
9550
9550
|
});
|
|
9551
9551
|
}
|
|
9552
9552
|
|
|
9553
|
+
// src/cli/commands/dlp/transfer.ts
|
|
9554
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9555
|
+
import { readFile as readFile9, stat } from "fs/promises";
|
|
9556
|
+
import { extname, resolve as resolve5 } from "path";
|
|
9557
|
+
import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9558
|
+
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
9559
|
+
import { ZodError } from "zod";
|
|
9560
|
+
|
|
9561
|
+
// src/backup/dlp-resources.ts
|
|
9562
|
+
import {
|
|
9563
|
+
AdvancedDataProfileRequestSchema,
|
|
9564
|
+
DataPatternRequestSchema,
|
|
9565
|
+
DataPatternResponseSchema,
|
|
9566
|
+
DataProfileResponseSchema,
|
|
9567
|
+
DictionaryRequestSchema,
|
|
9568
|
+
DictionaryResponseSchema
|
|
9569
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
9570
|
+
import { z as z5 } from "zod";
|
|
9571
|
+
var DLP_RESOURCE_KINDS = ["dictionaries", "patterns", "profiles"];
|
|
9572
|
+
var DlpResourcesBackupSchema = z5.object({
|
|
9573
|
+
version: z5.literal(1),
|
|
9574
|
+
resourceType: z5.literal("dlp-resources"),
|
|
9575
|
+
exportedAt: z5.string().datetime({ offset: true }),
|
|
9576
|
+
source: z5.object({ tsgId: z5.string().min(1) }).strict(),
|
|
9577
|
+
dictionaries: z5.array(DictionaryResponseSchema).max(1e4),
|
|
9578
|
+
patterns: z5.array(DataPatternResponseSchema).max(1e4),
|
|
9579
|
+
profiles: z5.array(DataProfileResponseSchema).max(1e4)
|
|
9580
|
+
}).strict();
|
|
9581
|
+
function object(value) {
|
|
9582
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9583
|
+
}
|
|
9584
|
+
var PORTABLE_TECHNIQUES = /* @__PURE__ */ new Set(["regex", "weighted_regex", "file_property"]);
|
|
9585
|
+
var RETIRED_PATTERN = /* @__PURE__ */ new Set(["deleted", "disabled"]);
|
|
9586
|
+
var RETIRED_PROFILE = /* @__PURE__ */ new Set(["deleted", "disabled"]);
|
|
9587
|
+
var SERVER_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
9588
|
+
"id",
|
|
9589
|
+
"version",
|
|
9590
|
+
"status",
|
|
9591
|
+
"profile_status",
|
|
9592
|
+
"tenant_id",
|
|
9593
|
+
"license_type",
|
|
9594
|
+
"is_parent_managed",
|
|
9595
|
+
"audit_metadata",
|
|
9596
|
+
"type",
|
|
9597
|
+
"advance_data_patterns_rule_request",
|
|
9598
|
+
"dictionary_metadata",
|
|
9599
|
+
"detection_technique",
|
|
9600
|
+
"detection_sub_technique",
|
|
9601
|
+
"attributes",
|
|
9602
|
+
"keywords",
|
|
9603
|
+
"tags"
|
|
9604
|
+
]);
|
|
9605
|
+
function pagesLimit(value) {
|
|
9606
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
|
|
9607
|
+
throw new Error("maxPages must be an integer from 1 to 1000");
|
|
9608
|
+
}
|
|
9609
|
+
function assertName(name) {
|
|
9610
|
+
if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9611
|
+
throw new Error("DLP resource names must be nonempty and contain no control characters");
|
|
9612
|
+
}
|
|
9613
|
+
async function springPages(fetch, maxPages3, label) {
|
|
9614
|
+
pagesLimit(maxPages3);
|
|
9615
|
+
const items = [];
|
|
9616
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
9617
|
+
const result = await fetch(page);
|
|
9618
|
+
items.push(...result.content);
|
|
9619
|
+
if (items.length > 1e4)
|
|
9620
|
+
throw new Error(`Inventory of ${label} exceeds the 10000-record safety limit`);
|
|
9621
|
+
const last = result.last ?? (result.totalPages !== void 0 ? page + 1 >= result.totalPages : result.content.length < 100);
|
|
9622
|
+
if (last) return items;
|
|
9623
|
+
if (!result.content.length) throw new Error(`Inventory of ${label} did not advance`);
|
|
9624
|
+
}
|
|
9625
|
+
throw new Error(`Inventory of ${label} is incomplete: increase --max-pages`);
|
|
9626
|
+
}
|
|
9627
|
+
async function inventories(api, maxPages3) {
|
|
9628
|
+
return {
|
|
9629
|
+
dictionaries: await springPages(
|
|
9630
|
+
(page) => api.dictionaries.list({ page, size: 100, keywords: true }),
|
|
9631
|
+
maxPages3,
|
|
9632
|
+
"dictionaries"
|
|
9633
|
+
),
|
|
9634
|
+
patterns: await springPages(
|
|
9635
|
+
(page) => api.patterns.list({ page, size: 100 }),
|
|
9636
|
+
maxPages3,
|
|
9637
|
+
"data patterns"
|
|
9638
|
+
),
|
|
9639
|
+
profiles: await springPages(
|
|
9640
|
+
(page) => api.profiles.list({ page, size: 100 }),
|
|
9641
|
+
maxPages3,
|
|
9642
|
+
"data profiles"
|
|
9643
|
+
)
|
|
9644
|
+
};
|
|
9645
|
+
}
|
|
9646
|
+
function canonical(value) {
|
|
9647
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object(v) ? Object.fromEntries(
|
|
9648
|
+
Object.entries(v).filter(([, child]) => child !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9649
|
+
) : v;
|
|
9650
|
+
return JSON.stringify(order(value));
|
|
9651
|
+
}
|
|
9652
|
+
function tenantIdentity(records) {
|
|
9653
|
+
const values = new Set(
|
|
9654
|
+
records.map((record) => record.tenant_id).filter((value) => value != null)
|
|
9655
|
+
);
|
|
9656
|
+
if (values.size > 1) throw new Error("Inventory reports mixed DLP tenant identities");
|
|
9657
|
+
return [...values][0];
|
|
9658
|
+
}
|
|
9659
|
+
function ruleLeaves(profile) {
|
|
9660
|
+
const leaves = [];
|
|
9661
|
+
const visit = (node, depth) => {
|
|
9662
|
+
if (depth > 100) throw new Error("Unsupported detection rule: expression tree too deep");
|
|
9663
|
+
if (!object(node)) return;
|
|
9664
|
+
if (object(node.rule_item)) {
|
|
9665
|
+
if (typeof node.rule_item.detection_technique !== "string")
|
|
9666
|
+
throw new Error("Unsupported detection rule: leaf without a detection technique");
|
|
9667
|
+
leaves.push({ value: node.rule_item, technique: node.rule_item.detection_technique });
|
|
9668
|
+
}
|
|
9669
|
+
if (Array.isArray(node.sub_expressions))
|
|
9670
|
+
for (const child of node.sub_expressions) visit(child, depth + 1);
|
|
9671
|
+
};
|
|
9672
|
+
for (const rule of profile.detection_rules ?? []) {
|
|
9673
|
+
if (!object(rule)) throw new Error("Unsupported detection rule shape");
|
|
9674
|
+
if (rule.rule_type !== "expression_tree")
|
|
9675
|
+
throw new Error(`Unsupported detection rule type: ${String(rule.rule_type)}`);
|
|
9676
|
+
visit(rule.expression_tree, 0);
|
|
9677
|
+
}
|
|
9678
|
+
return leaves;
|
|
9679
|
+
}
|
|
9680
|
+
function assertKnownReferences(body, known, label) {
|
|
9681
|
+
const visit = (value, path3) => {
|
|
9682
|
+
if (Array.isArray(value)) {
|
|
9683
|
+
value.forEach((v, i) => {
|
|
9684
|
+
visit(v, `${path3}[${i}]`);
|
|
9685
|
+
});
|
|
9686
|
+
return;
|
|
9687
|
+
}
|
|
9688
|
+
if (!object(value)) return;
|
|
9689
|
+
for (const [key, child] of Object.entries(value)) {
|
|
9690
|
+
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !(known.has(value) && key === "id"))
|
|
9691
|
+
throw new Error(`Unsupported cross-tenant reference in ${label} at ${path3}.${key}`);
|
|
9692
|
+
visit(child, `${path3}.${key}`);
|
|
9693
|
+
}
|
|
9694
|
+
};
|
|
9695
|
+
visit(body, label);
|
|
9696
|
+
}
|
|
9697
|
+
function uniqueNames(records, label) {
|
|
9698
|
+
const names = /* @__PURE__ */ new Set();
|
|
9699
|
+
for (const record of records) {
|
|
9700
|
+
const name = record.name;
|
|
9701
|
+
if (!name) throw new Error(`Backup contains a ${label} without a name`);
|
|
9702
|
+
assertName(name);
|
|
9703
|
+
if (names.has(name)) throw new Error(`Backup contains duplicate ${label} names: ${name}`);
|
|
9704
|
+
names.add(name);
|
|
9705
|
+
}
|
|
9706
|
+
}
|
|
9707
|
+
async function backupDlpResources(api, tsgId, options = {}) {
|
|
9708
|
+
if (!tsgId) throw new Error("Source TSG ID is required");
|
|
9709
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
9710
|
+
const kinds = options.resources ?? [...DLP_RESOURCE_KINDS];
|
|
9711
|
+
if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)))
|
|
9712
|
+
throw new Error("resources must name dictionaries, patterns and/or profiles");
|
|
9713
|
+
const inventory2 = await inventories(api, maxPages3);
|
|
9714
|
+
tenantIdentity([...inventory2.patterns, ...inventory2.profiles]);
|
|
9715
|
+
const dictionaries = /* @__PURE__ */ new Map();
|
|
9716
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
9717
|
+
const addDictionary = (record) => {
|
|
9718
|
+
if (!record.id) throw new Error("Inventory dictionary has no id");
|
|
9719
|
+
if (record.type !== "predefined" && !Array.isArray(record.keywords))
|
|
9720
|
+
throw new Error(`Dictionary keywords were not returned by the region: ${record.name}`);
|
|
9721
|
+
dictionaries.set(record.id, structuredClone(record));
|
|
9722
|
+
};
|
|
9723
|
+
const addPattern = (record) => {
|
|
9724
|
+
if (!record.id) throw new Error("Inventory data pattern has no id");
|
|
9725
|
+
patterns.set(record.id, structuredClone(record));
|
|
9726
|
+
};
|
|
9727
|
+
if (kinds.includes("dictionaries")) {
|
|
9728
|
+
for (const record of inventory2.dictionaries)
|
|
9729
|
+
if (record.type !== "predefined") addDictionary(record);
|
|
9730
|
+
}
|
|
9731
|
+
if (kinds.includes("patterns")) {
|
|
9732
|
+
for (const record of inventory2.patterns)
|
|
9733
|
+
if (record.type !== "predefined" && !RETIRED_PATTERN.has(record.status ?? "active"))
|
|
9734
|
+
addPattern(record);
|
|
9735
|
+
}
|
|
9736
|
+
const profiles2 = [];
|
|
9737
|
+
const skipped = [];
|
|
9738
|
+
if (kinds.includes("profiles")) {
|
|
9739
|
+
const patternById = new Map(inventory2.patterns.map((p) => [p.id ?? "", p]));
|
|
9740
|
+
const dictionaryById = new Map(inventory2.dictionaries.map((d) => [d.id ?? "", d]));
|
|
9741
|
+
for (const profile of inventory2.profiles) {
|
|
9742
|
+
if (profile.type === "predefined") continue;
|
|
9743
|
+
if (RETIRED_PROFILE.has(profile.profile_status ?? "active")) continue;
|
|
9744
|
+
const name = profile.name ?? "(unnamed)";
|
|
9745
|
+
try {
|
|
9746
|
+
for (const leaf of ruleLeaves(profile)) {
|
|
9747
|
+
const id = leaf.value.id;
|
|
9748
|
+
if (id == null || id === "") {
|
|
9749
|
+
if (leaf.value.edm_dataset_id != null)
|
|
9750
|
+
throw new Error("references an EDM dataset directly");
|
|
9751
|
+
continue;
|
|
9752
|
+
}
|
|
9753
|
+
if (typeof id !== "string") throw new Error("has a non-string rule reference id");
|
|
9754
|
+
if (leaf.technique === "dictionary") {
|
|
9755
|
+
const record = dictionaryById.get(id);
|
|
9756
|
+
if (!record) throw new Error(`references an unknown dictionary: ${id}`);
|
|
9757
|
+
if (leaf.value.version != null)
|
|
9758
|
+
throw new Error(`references a version-pinned dictionary: ${id}`);
|
|
9759
|
+
addDictionary(record);
|
|
9760
|
+
} else {
|
|
9761
|
+
const record = patternById.get(id);
|
|
9762
|
+
if (!record) throw new Error(`references an unknown data pattern: ${id}`);
|
|
9763
|
+
if (leaf.value.version != null && record.version !== leaf.value.version)
|
|
9764
|
+
throw new Error(
|
|
9765
|
+
`references data pattern version ${leaf.value.version}, but the catalog holds version ${record.version ?? "unknown"}: ${id}`
|
|
9766
|
+
);
|
|
9767
|
+
addPattern(record);
|
|
9768
|
+
}
|
|
9769
|
+
}
|
|
9770
|
+
profiles2.push(structuredClone(profile));
|
|
9771
|
+
} catch (error) {
|
|
9772
|
+
const reason = error instanceof Error ? error.message : "unsupported rule";
|
|
9773
|
+
skipped.push({ profile: name, reason });
|
|
9774
|
+
}
|
|
9775
|
+
}
|
|
9776
|
+
if (skipped.length && !options.skipUnsupported)
|
|
9777
|
+
throw new Error(
|
|
9778
|
+
`Profiles cannot be exported: ${skipped.map((item) => `${item.profile} (${item.reason})`).join("; ")}; exclude unsupported profiles explicitly with --skip-unsupported`
|
|
9779
|
+
);
|
|
9780
|
+
}
|
|
9781
|
+
const byName = (a, b) => (a.name ?? "").localeCompare(b.name ?? "");
|
|
9782
|
+
const dictionaryRecords = [...dictionaries.values()].sort(byName);
|
|
9783
|
+
const patternRecords = [...patterns.values()].sort(byName);
|
|
9784
|
+
profiles2.sort(byName);
|
|
9785
|
+
uniqueNames(dictionaryRecords, "dictionary");
|
|
9786
|
+
uniqueNames(patternRecords, "data pattern");
|
|
9787
|
+
uniqueNames(profiles2, "data profile");
|
|
9788
|
+
if (!dictionaryRecords.length && !patternRecords.length && !profiles2.length)
|
|
9789
|
+
throw new Error("No matching DLP resources to back up");
|
|
9790
|
+
const backup = DlpResourcesBackupSchema.parse({
|
|
9791
|
+
version: 1,
|
|
9792
|
+
resourceType: "dlp-resources",
|
|
9793
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9794
|
+
source: { tsgId },
|
|
9795
|
+
dictionaries: dictionaryRecords,
|
|
9796
|
+
patterns: patternRecords,
|
|
9797
|
+
profiles: profiles2
|
|
9798
|
+
});
|
|
9799
|
+
return { backup, skipped };
|
|
9800
|
+
}
|
|
9801
|
+
function compareEcho(request, readBack) {
|
|
9802
|
+
const differences = [];
|
|
9803
|
+
const serverAdded = [];
|
|
9804
|
+
const visit = (source, target, path3, top) => {
|
|
9805
|
+
const a = source ?? null;
|
|
9806
|
+
const b = target ?? null;
|
|
9807
|
+
if (a === null && b === null) return;
|
|
9808
|
+
if (a === null) {
|
|
9809
|
+
serverAdded.push(path3);
|
|
9810
|
+
return;
|
|
9811
|
+
}
|
|
9812
|
+
if (b === null) {
|
|
9813
|
+
differences.push(path3);
|
|
9814
|
+
return;
|
|
9815
|
+
}
|
|
9816
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
9817
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
9818
|
+
differences.push(path3);
|
|
9819
|
+
return;
|
|
9820
|
+
}
|
|
9821
|
+
a.forEach((item, index) => {
|
|
9822
|
+
visit(item, b[index], `${path3}[${index}]`, false);
|
|
9823
|
+
});
|
|
9824
|
+
return;
|
|
9825
|
+
}
|
|
9826
|
+
if (object(a) && object(b)) {
|
|
9827
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
9828
|
+
if (top && !(key in a) && SERVER_METADATA_KEYS.has(key)) continue;
|
|
9829
|
+
visit(a[key], b[key], `${path3}.${key}`, false);
|
|
9830
|
+
}
|
|
9831
|
+
return;
|
|
9832
|
+
}
|
|
9833
|
+
if (a !== b) differences.push(path3);
|
|
9834
|
+
};
|
|
9835
|
+
visit(request, readBack, "body", true);
|
|
9836
|
+
return { matches: differences.length === 0, differences, serverAdded };
|
|
9837
|
+
}
|
|
9838
|
+
function keywordSet(value) {
|
|
9839
|
+
if (!Array.isArray(value) || value.some((keyword) => typeof keyword !== "string"))
|
|
9840
|
+
return void 0;
|
|
9841
|
+
return [...value].sort();
|
|
9842
|
+
}
|
|
9843
|
+
function dictionaryEcho(metadata, record, keywords) {
|
|
9844
|
+
const { original_file_name, ...projection } = metadata;
|
|
9845
|
+
const echo = compareEcho(projection, record);
|
|
9846
|
+
const echoedFile = record?.dictionary_metadata?.original_file_name;
|
|
9847
|
+
const echoedKeywords = keywordSet(record?.keywords);
|
|
9848
|
+
const matches = echo.matches && (echoedFile == null || echoedFile === original_file_name) && echoedKeywords !== void 0 && canonical(echoedKeywords) === canonical(keywords);
|
|
9849
|
+
return { matches, serverAdded: echo.serverAdded };
|
|
9850
|
+
}
|
|
9851
|
+
function dictionaryRequest(source, name) {
|
|
9852
|
+
const metadata = {
|
|
9853
|
+
category: source.category,
|
|
9854
|
+
name,
|
|
9855
|
+
original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
|
|
9856
|
+
region_name: source.region_name,
|
|
9857
|
+
...source.description != null ? { description: source.description } : {},
|
|
9858
|
+
...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {}
|
|
9859
|
+
};
|
|
9860
|
+
const parsed = DictionaryRequestSchema.safeParse(metadata);
|
|
9861
|
+
if (!parsed.success)
|
|
9862
|
+
throw new Error(`Dictionary cannot be rebuilt from the backup: ${source.name}`);
|
|
9863
|
+
return parsed.data;
|
|
9864
|
+
}
|
|
9865
|
+
function patternRequest(source, name) {
|
|
9866
|
+
const body = {
|
|
9867
|
+
name,
|
|
9868
|
+
type: source.type,
|
|
9869
|
+
detection_config: source.detection_config,
|
|
9870
|
+
...source.matching_rules != null ? { matching_rules: source.matching_rules } : {},
|
|
9871
|
+
...source.description != null ? { description: source.description } : {},
|
|
9872
|
+
...source.tags != null ? { tags: source.tags } : {}
|
|
9873
|
+
};
|
|
9874
|
+
const parsed = DataPatternRequestSchema.safeParse(structuredClone(body));
|
|
9875
|
+
if (!parsed.success)
|
|
9876
|
+
throw new Error(`Data pattern cannot be rebuilt from the backup: ${source.name}`);
|
|
9877
|
+
return parsed.data;
|
|
9878
|
+
}
|
|
9879
|
+
function portablePattern(record) {
|
|
9880
|
+
return PORTABLE_TECHNIQUES.has(record.detection_config?.technique ?? "") && record.detection_config?.technique !== void 0;
|
|
9881
|
+
}
|
|
9882
|
+
function matchByName(records, name, retired, label) {
|
|
9883
|
+
const matches = records.filter((record) => record.name === name && !retired(record));
|
|
9884
|
+
if (matches.length > 1) throw new Error(`Ambiguous destination ${label} name: ${name}`);
|
|
9885
|
+
return matches[0];
|
|
9886
|
+
}
|
|
9887
|
+
async function planDlpResourcesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9888
|
+
const backup = DlpResourcesBackupSchema.parse(input2);
|
|
9889
|
+
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9890
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
9891
|
+
pagesLimit(maxPages3);
|
|
9892
|
+
const prefix = options.namePrefix ?? "";
|
|
9893
|
+
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9894
|
+
throw new Error("Invalid --name-prefix");
|
|
9895
|
+
const conflict = options.onConflict ?? "error";
|
|
9896
|
+
if (!["error", "verify", "skip"].includes(conflict)) throw new Error("Invalid conflict policy");
|
|
9897
|
+
const patternMap2 = options.patternMap ?? {};
|
|
9898
|
+
uniqueNames(backup.dictionaries, "dictionary");
|
|
9899
|
+
uniqueNames(backup.patterns, "data pattern");
|
|
9900
|
+
uniqueNames(backup.profiles, "data profile");
|
|
9901
|
+
const unknownMapped = Object.keys(patternMap2).filter(
|
|
9902
|
+
(name) => !backup.patterns.some((p) => p.name === name)
|
|
9903
|
+
);
|
|
9904
|
+
if (unknownMapped.length)
|
|
9905
|
+
throw new Error(`--pattern-map names absent from the backup: ${unknownMapped.join(", ")}`);
|
|
9906
|
+
const crossTenant = backup.source.tsgId !== destinationTsgId;
|
|
9907
|
+
const destination = await inventories(api, maxPages3);
|
|
9908
|
+
const sourceTenant = tenantIdentity([...backup.patterns, ...backup.profiles]);
|
|
9909
|
+
const destinationTenant = tenantIdentity([...destination.patterns, ...destination.profiles]);
|
|
9910
|
+
if (sourceTenant !== void 0 && destinationTenant !== void 0) {
|
|
9911
|
+
if (crossTenant && sourceTenant === destinationTenant)
|
|
9912
|
+
throw new Error(
|
|
9913
|
+
"Destination credentials report the source DLP tenant; check the selected tenant before restoring"
|
|
9914
|
+
);
|
|
9915
|
+
if (!crossTenant && sourceTenant !== destinationTenant)
|
|
9916
|
+
throw new Error(
|
|
9917
|
+
"Destination DLP tenant does not match the backup source; use distinct tenant credentials deliberately"
|
|
9918
|
+
);
|
|
9919
|
+
}
|
|
9920
|
+
const dictionaryPlans = [];
|
|
9921
|
+
for (const source of backup.dictionaries) {
|
|
9922
|
+
if (!source.id || !source.name) throw new Error("Backup dictionary lacks identity");
|
|
9923
|
+
if (source.type === "predefined") {
|
|
9924
|
+
const match2 = matchByName(destination.dictionaries, source.name, () => false, "dictionary");
|
|
9925
|
+
if (!match2?.id || match2.type !== "predefined")
|
|
9926
|
+
throw new Error(`Missing predefined destination dictionary: ${source.name}`);
|
|
9927
|
+
dictionaryPlans.push({
|
|
9928
|
+
sourceId: source.id,
|
|
9929
|
+
name: source.name,
|
|
9930
|
+
action: "resolve",
|
|
9931
|
+
existing: structuredClone(match2)
|
|
9932
|
+
});
|
|
9933
|
+
continue;
|
|
9934
|
+
}
|
|
9935
|
+
const name = `${prefix}${source.name}`;
|
|
9936
|
+
assertName(name);
|
|
9937
|
+
const metadata = dictionaryRequest(source, name);
|
|
9938
|
+
const keywords = keywordSet(source.keywords);
|
|
9939
|
+
if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
|
|
9940
|
+
const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
|
|
9941
|
+
if (!match) {
|
|
9942
|
+
dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
|
|
9943
|
+
continue;
|
|
9944
|
+
}
|
|
9945
|
+
if (!match.id) throw new Error(`Destination dictionary has no identity: ${name}`);
|
|
9946
|
+
if (!dictionaryEcho(metadata, match, keywords).matches)
|
|
9947
|
+
throw new Error(
|
|
9948
|
+
`Destination dictionary differs: ${name}; use --name-prefix to avoid changing a shared dictionary`
|
|
9949
|
+
);
|
|
9950
|
+
dictionaryPlans.push({
|
|
9951
|
+
sourceId: source.id,
|
|
9952
|
+
name,
|
|
9953
|
+
action: "reuse",
|
|
9954
|
+
metadata,
|
|
9955
|
+
keywords,
|
|
9956
|
+
existing: structuredClone(match)
|
|
9957
|
+
});
|
|
9958
|
+
}
|
|
9959
|
+
const patternPlans = [];
|
|
9960
|
+
for (const source of backup.patterns) {
|
|
9961
|
+
if (!source.id || !source.name) throw new Error("Backup data pattern lacks identity");
|
|
9962
|
+
const retired = (record) => RETIRED_PATTERN.has(record.status ?? "active");
|
|
9963
|
+
if (source.type === "predefined") {
|
|
9964
|
+
const match2 = matchByName(destination.patterns, source.name, retired, "data pattern");
|
|
9965
|
+
if (!match2?.id || match2.type !== "predefined")
|
|
9966
|
+
throw new Error(`Missing predefined destination data pattern: ${source.name}`);
|
|
9967
|
+
patternPlans.push({
|
|
9968
|
+
sourceId: source.id,
|
|
9969
|
+
name: source.name,
|
|
9970
|
+
action: "resolve",
|
|
9971
|
+
existing: structuredClone(match2)
|
|
9972
|
+
});
|
|
9973
|
+
continue;
|
|
9974
|
+
}
|
|
9975
|
+
if (Object.hasOwn(patternMap2, source.name)) {
|
|
9976
|
+
const target = patternMap2[source.name];
|
|
9977
|
+
const match2 = matchByName(destination.patterns, target, retired, "data pattern");
|
|
9978
|
+
if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
|
|
9979
|
+
patternPlans.push({
|
|
9980
|
+
sourceId: source.id,
|
|
9981
|
+
name: target,
|
|
9982
|
+
action: "map",
|
|
9983
|
+
existing: structuredClone(match2)
|
|
9984
|
+
});
|
|
9985
|
+
continue;
|
|
9986
|
+
}
|
|
9987
|
+
if (!portablePattern(source))
|
|
9988
|
+
throw new Error(
|
|
9989
|
+
`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>"`
|
|
9990
|
+
);
|
|
9991
|
+
const name = `${prefix}${source.name}`;
|
|
9992
|
+
assertName(name);
|
|
9993
|
+
const body = patternRequest(source, name);
|
|
9994
|
+
if (crossTenant) assertKnownReferences(body, /* @__PURE__ */ new Set(), `pattern ${source.name}`);
|
|
9995
|
+
const match = matchByName(destination.patterns, name, retired, "data pattern");
|
|
9996
|
+
if (!match) {
|
|
9997
|
+
patternPlans.push({ sourceId: source.id, name, action: "create", body });
|
|
9998
|
+
continue;
|
|
9999
|
+
}
|
|
10000
|
+
if (!match.id) throw new Error(`Destination data pattern has no identity: ${name}`);
|
|
10001
|
+
if (!compareEcho(body, match).matches)
|
|
10002
|
+
throw new Error(
|
|
10003
|
+
`Destination data pattern differs: ${name}; use --name-prefix to avoid changing a shared pattern`
|
|
10004
|
+
);
|
|
10005
|
+
patternPlans.push({
|
|
10006
|
+
sourceId: source.id,
|
|
10007
|
+
name,
|
|
10008
|
+
action: "reuse",
|
|
10009
|
+
body,
|
|
10010
|
+
existing: structuredClone(match)
|
|
10011
|
+
});
|
|
10012
|
+
}
|
|
10013
|
+
const profilePlans = [];
|
|
10014
|
+
for (const source of backup.profiles) {
|
|
10015
|
+
if (!source.name) throw new Error("Backup data profile lacks identity");
|
|
10016
|
+
if (source.type === "predefined") throw new Error("Backup contains a predefined data profile");
|
|
10017
|
+
const name = `${prefix}${source.name}`;
|
|
10018
|
+
assertName(name);
|
|
10019
|
+
const match = matchByName(
|
|
10020
|
+
destination.profiles,
|
|
10021
|
+
name,
|
|
10022
|
+
(record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
|
|
10023
|
+
"data profile"
|
|
10024
|
+
);
|
|
10025
|
+
if (match && conflict === "error")
|
|
10026
|
+
throw new Error(
|
|
10027
|
+
`Destination data profile already exists: ${name}; choose --on-conflict verify or skip`
|
|
10028
|
+
);
|
|
10029
|
+
if (match && !match.id) throw new Error(`Destination data profile has no identity: ${name}`);
|
|
10030
|
+
profilePlans.push({
|
|
10031
|
+
name,
|
|
10032
|
+
source: structuredClone(source),
|
|
10033
|
+
existing: match ? structuredClone(match) : void 0,
|
|
10034
|
+
action: match ? conflict === "skip" ? "skip" : "verify" : "create"
|
|
10035
|
+
});
|
|
10036
|
+
}
|
|
10037
|
+
const plan = {
|
|
10038
|
+
sourceTsgId: backup.source.tsgId,
|
|
10039
|
+
destinationTsgId,
|
|
10040
|
+
dictionaries: dictionaryPlans,
|
|
10041
|
+
patterns: patternPlans,
|
|
10042
|
+
profiles: profilePlans,
|
|
10043
|
+
maxPages: maxPages3
|
|
10044
|
+
};
|
|
10045
|
+
const bindings = planBindings(plan);
|
|
10046
|
+
const planned = /* @__PURE__ */ new Set([
|
|
10047
|
+
...dictionaryPlans.map((d) => d.sourceId),
|
|
10048
|
+
...patternPlans.map((p) => p.sourceId)
|
|
10049
|
+
]);
|
|
10050
|
+
for (const item of profilePlans) {
|
|
10051
|
+
if (item.action === "skip") continue;
|
|
10052
|
+
for (const leaf of ruleLeaves(item.source)) {
|
|
10053
|
+
const id = leaf.value.id;
|
|
10054
|
+
if (id != null && id !== "" && !planned.has(String(id)))
|
|
10055
|
+
throw new Error(
|
|
10056
|
+
`Backup data profile references a resource absent from the backup: ${item.name}`
|
|
10057
|
+
);
|
|
10058
|
+
}
|
|
10059
|
+
const body = profileRequest(item, bindings, crossTenant, item.action === "verify");
|
|
10060
|
+
if (item.action === "verify" && body && !compareEcho(body, item.existing).matches)
|
|
10061
|
+
throw new Error(
|
|
10062
|
+
`Destination data profile does not match restore plan: ${item.name}; verification made no changes`
|
|
10063
|
+
);
|
|
10064
|
+
}
|
|
10065
|
+
return plan;
|
|
10066
|
+
}
|
|
10067
|
+
function planBindings(plan) {
|
|
10068
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
10069
|
+
for (const item of plan.dictionaries)
|
|
10070
|
+
if (item.existing?.id) bindings.set(item.sourceId, { id: item.existing.id, name: item.name });
|
|
10071
|
+
for (const item of plan.patterns)
|
|
10072
|
+
if (item.existing?.id)
|
|
10073
|
+
bindings.set(item.sourceId, {
|
|
10074
|
+
id: item.existing.id,
|
|
10075
|
+
name: item.name,
|
|
10076
|
+
version: item.existing.version ?? void 0
|
|
10077
|
+
});
|
|
10078
|
+
return bindings;
|
|
10079
|
+
}
|
|
10080
|
+
function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
10081
|
+
const detectionRules = structuredClone(item.source.detection_rules ?? []);
|
|
10082
|
+
const known = /* @__PURE__ */ new Set();
|
|
10083
|
+
let deferred = false;
|
|
10084
|
+
const remapped = { ...structuredClone(item.source), detection_rules: detectionRules };
|
|
10085
|
+
for (const leaf of ruleLeaves(remapped)) {
|
|
10086
|
+
known.add(leaf.value);
|
|
10087
|
+
const id = leaf.value.id;
|
|
10088
|
+
if (id == null || id === "") continue;
|
|
10089
|
+
const binding = bindings.get(String(id));
|
|
10090
|
+
if (!binding) {
|
|
10091
|
+
if (requireBindings)
|
|
10092
|
+
throw new Error(
|
|
10093
|
+
`Missing planned dependency binding; an existing data profile cannot be verified without its destination dependencies: ${item.name}`
|
|
10094
|
+
);
|
|
10095
|
+
deferred = true;
|
|
10096
|
+
continue;
|
|
10097
|
+
}
|
|
10098
|
+
leaf.value.id = binding.id;
|
|
10099
|
+
if (leaf.value.name != null) leaf.value.name = binding.name;
|
|
10100
|
+
if (leaf.value.version != null) {
|
|
10101
|
+
if (binding.version === void 0)
|
|
10102
|
+
throw new Error(`Destination data pattern version is unknown: ${binding.name}`);
|
|
10103
|
+
leaf.value.version = binding.version;
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
const body = {
|
|
10107
|
+
name: item.name,
|
|
10108
|
+
detection_rules: detectionRules,
|
|
10109
|
+
...item.source.profile_type != null ? { profile_type: item.source.profile_type } : {},
|
|
10110
|
+
...item.source.description != null ? { description: item.source.description } : {},
|
|
10111
|
+
...item.source.is_granular_data_profile != null ? { is_granular_data_profile: item.source.is_granular_data_profile } : {}
|
|
10112
|
+
};
|
|
10113
|
+
if (crossTenant) assertKnownReferences(body, known, `profile ${item.name}`);
|
|
10114
|
+
const parsed = AdvancedDataProfileRequestSchema.parse(body);
|
|
10115
|
+
return deferred ? void 0 : parsed;
|
|
10116
|
+
}
|
|
10117
|
+
var DEPENDENCY_ACTIONS = {
|
|
10118
|
+
create: "created",
|
|
10119
|
+
reuse: "reused",
|
|
10120
|
+
resolve: "resolved",
|
|
10121
|
+
map: "mapped"
|
|
10122
|
+
};
|
|
10123
|
+
function assertUnchanged(current, prior, label) {
|
|
10124
|
+
if (canonical(current ?? null) !== canonical(prior ?? null))
|
|
10125
|
+
throw new Error(`Destination ${label} changed after planning`);
|
|
10126
|
+
}
|
|
10127
|
+
async function restoreDlpResources(api, plan) {
|
|
10128
|
+
const result = {
|
|
10129
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10130
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10131
|
+
complete: false,
|
|
10132
|
+
dictionaries: [],
|
|
10133
|
+
patterns: [],
|
|
10134
|
+
profiles: [],
|
|
10135
|
+
serverAdded: []
|
|
10136
|
+
};
|
|
10137
|
+
const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
|
|
10138
|
+
try {
|
|
10139
|
+
const current = await inventories(api, plan.maxPages);
|
|
10140
|
+
for (const item of plan.dictionaries) {
|
|
10141
|
+
const match = matchByName(current.dictionaries, item.name, () => false, "dictionary");
|
|
10142
|
+
if (item.existing) assertUnchanged(match, item.existing, `dictionary: ${item.name}`);
|
|
10143
|
+
else if (match)
|
|
10144
|
+
throw new Error(`Destination dictionary changed after planning: ${item.name}`);
|
|
10145
|
+
}
|
|
10146
|
+
const retiredPattern = (record) => RETIRED_PATTERN.has(record.status ?? "active");
|
|
10147
|
+
for (const item of plan.patterns) {
|
|
10148
|
+
const match = matchByName(current.patterns, item.name, retiredPattern, "data pattern");
|
|
10149
|
+
if (item.existing) assertUnchanged(match, item.existing, `data pattern: ${item.name}`);
|
|
10150
|
+
else if (match)
|
|
10151
|
+
throw new Error(`Destination data pattern changed after planning: ${item.name}`);
|
|
10152
|
+
}
|
|
10153
|
+
for (const item of plan.profiles) {
|
|
10154
|
+
const match = matchByName(
|
|
10155
|
+
current.profiles,
|
|
10156
|
+
item.name,
|
|
10157
|
+
(record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
|
|
10158
|
+
"data profile"
|
|
10159
|
+
);
|
|
10160
|
+
if (item.existing) assertUnchanged(match, item.existing, `data profile: ${item.name}`);
|
|
10161
|
+
else if (match)
|
|
10162
|
+
throw new Error(`Destination data profile changed after planning: ${item.name}`);
|
|
10163
|
+
}
|
|
10164
|
+
const bindings = planBindings(plan);
|
|
10165
|
+
for (const item of plan.dictionaries) {
|
|
10166
|
+
if (item.action !== "create") {
|
|
10167
|
+
const id = item.existing?.id;
|
|
10168
|
+
if (!id) throw new Error(`Destination dictionary has no identity: ${item.name}`);
|
|
10169
|
+
result.dictionaries.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
|
|
10170
|
+
continue;
|
|
10171
|
+
}
|
|
10172
|
+
if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
|
|
10173
|
+
const created = await api.dictionaries.create({
|
|
10174
|
+
metadata: item.metadata,
|
|
10175
|
+
file: `${item.keywords.join("\n")}
|
|
10176
|
+
`,
|
|
10177
|
+
includeKeywords: true
|
|
10178
|
+
});
|
|
10179
|
+
if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10180
|
+
const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
|
|
10181
|
+
const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
|
|
10182
|
+
if (!echo.matches || readBack.name !== item.name)
|
|
10183
|
+
throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10184
|
+
if (echo.serverAdded.length)
|
|
10185
|
+
result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
|
|
10186
|
+
bindings.set(item.sourceId, { id: created.id, name: item.name });
|
|
10187
|
+
result.dictionaries.push({ name: item.name, action: "created", id: created.id });
|
|
10188
|
+
}
|
|
10189
|
+
for (const item of plan.patterns) {
|
|
10190
|
+
if (item.action !== "create") {
|
|
10191
|
+
const id = item.existing?.id;
|
|
10192
|
+
if (!id) throw new Error(`Destination data pattern has no identity: ${item.name}`);
|
|
10193
|
+
result.patterns.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
|
|
10194
|
+
continue;
|
|
10195
|
+
}
|
|
10196
|
+
if (!item.body) throw new Error("Data pattern plan is incomplete");
|
|
10197
|
+
const created = await api.patterns.create(item.body);
|
|
10198
|
+
if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10199
|
+
const readBack = await api.patterns.get(created.id);
|
|
10200
|
+
const echo = compareEcho(item.body, readBack);
|
|
10201
|
+
if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10202
|
+
throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10203
|
+
if (echo.serverAdded.length)
|
|
10204
|
+
result.serverAdded.push({
|
|
10205
|
+
resource: `data pattern ${item.name}`,
|
|
10206
|
+
fields: echo.serverAdded
|
|
10207
|
+
});
|
|
10208
|
+
bindings.set(item.sourceId, {
|
|
10209
|
+
id: created.id,
|
|
10210
|
+
name: item.name,
|
|
10211
|
+
version: readBack.version ?? void 0
|
|
10212
|
+
});
|
|
10213
|
+
result.patterns.push({ name: item.name, action: "created", id: created.id });
|
|
10214
|
+
}
|
|
10215
|
+
for (const item of plan.profiles) {
|
|
10216
|
+
if (item.action === "skip") {
|
|
10217
|
+
result.profiles.push({
|
|
10218
|
+
name: item.name,
|
|
10219
|
+
action: "skipped",
|
|
10220
|
+
id: item.existing?.id ?? void 0
|
|
10221
|
+
});
|
|
10222
|
+
continue;
|
|
10223
|
+
}
|
|
10224
|
+
const body = profileRequest(item, bindings, crossTenant, true);
|
|
10225
|
+
if (!body) throw new Error(`Missing planned dependency binding: ${item.name}`);
|
|
10226
|
+
if (item.action === "verify") {
|
|
10227
|
+
const echo2 = compareEcho(body, item.existing);
|
|
10228
|
+
if (!echo2.matches)
|
|
10229
|
+
throw new Error(`Destination data profile does not match restore plan: ${item.name}`);
|
|
10230
|
+
if (echo2.serverAdded.length)
|
|
10231
|
+
result.serverAdded.push({
|
|
10232
|
+
resource: `data profile ${item.name}`,
|
|
10233
|
+
fields: echo2.serverAdded
|
|
10234
|
+
});
|
|
10235
|
+
result.profiles.push({
|
|
10236
|
+
name: item.name,
|
|
10237
|
+
action: "verified",
|
|
10238
|
+
id: item.existing?.id ?? void 0
|
|
10239
|
+
});
|
|
10240
|
+
continue;
|
|
10241
|
+
}
|
|
10242
|
+
const created = await api.profiles.create(body);
|
|
10243
|
+
if (!created.id) throw new Error(`Restored data profile did not verify: ${item.name}`);
|
|
10244
|
+
const readBack = await api.profiles.get(created.id);
|
|
10245
|
+
const echo = compareEcho(body, readBack);
|
|
10246
|
+
if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10247
|
+
throw new Error(`Restored data profile did not verify: ${item.name}`);
|
|
10248
|
+
if (echo.serverAdded.length)
|
|
10249
|
+
result.serverAdded.push({
|
|
10250
|
+
resource: `data profile ${item.name}`,
|
|
10251
|
+
fields: echo.serverAdded
|
|
10252
|
+
});
|
|
10253
|
+
result.profiles.push({ name: item.name, action: "created", id: created.id });
|
|
10254
|
+
}
|
|
10255
|
+
result.complete = true;
|
|
10256
|
+
} catch (error) {
|
|
10257
|
+
const status = error?.statusCode;
|
|
10258
|
+
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(
|
|
10259
|
+
error.message
|
|
10260
|
+
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
10261
|
+
}
|
|
10262
|
+
return result;
|
|
10263
|
+
}
|
|
10264
|
+
|
|
10265
|
+
// src/cli/commands/dlp/transfer.ts
|
|
10266
|
+
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
10267
|
+
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
10268
|
+
function maxPages(value) {
|
|
10269
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
10270
|
+
usageError("--max-pages must be an integer from 1 to 1000");
|
|
10271
|
+
return Number(value);
|
|
10272
|
+
}
|
|
10273
|
+
function resourceKinds(value) {
|
|
10274
|
+
const kinds = value.split(",").map((kind) => kind.trim());
|
|
10275
|
+
if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)) || new Set(kinds).size !== kinds.length)
|
|
10276
|
+
throw new CliUsageError("--resources takes a unique list of dictionaries, patterns, profiles");
|
|
10277
|
+
return kinds;
|
|
10278
|
+
}
|
|
10279
|
+
function patternMap(values) {
|
|
10280
|
+
const entries = values.map((value) => {
|
|
10281
|
+
const at = value.indexOf("=");
|
|
10282
|
+
if (at < 1 || at === value.length - 1)
|
|
10283
|
+
throw new CliUsageError("--pattern-map requires source-name=destination-name");
|
|
10284
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
10285
|
+
});
|
|
10286
|
+
if (new Set(entries.map(([key]) => key)).size !== entries.length)
|
|
10287
|
+
throw new CliUsageError("Duplicate source names in --pattern-map");
|
|
10288
|
+
return Object.fromEntries(entries);
|
|
10289
|
+
}
|
|
10290
|
+
async function context() {
|
|
10291
|
+
const config = await loadConfig();
|
|
10292
|
+
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
10293
|
+
throw new CliUsageError(
|
|
10294
|
+
"DLP transfer requires configured Management client ID, secret and TSG ID"
|
|
10295
|
+
);
|
|
10296
|
+
const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
|
|
10297
|
+
return {
|
|
10298
|
+
api: {
|
|
10299
|
+
dictionaries: client.dlp.dictionaries,
|
|
10300
|
+
patterns: client.dlp.dataPatterns,
|
|
10301
|
+
profiles: client.dlp.dataProfiles
|
|
10302
|
+
},
|
|
10303
|
+
tsgId: config.mgmtTsgId
|
|
10304
|
+
};
|
|
10305
|
+
}
|
|
10306
|
+
function failure(error) {
|
|
10307
|
+
if (error instanceof CliUsageError) usageError(error.message);
|
|
10308
|
+
if (error instanceof ZodError) {
|
|
10309
|
+
ui.error("Invalid DLP backup/request schema; content is not logged.");
|
|
10310
|
+
} else if (error instanceof AISecSDKException2) {
|
|
10311
|
+
ui.error(
|
|
10312
|
+
`DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10313
|
+
);
|
|
10314
|
+
} else if (error?.code) {
|
|
10315
|
+
ui.error(
|
|
10316
|
+
`DLP transfer file operation failed (${error.code}); existing files are not overwritten.`
|
|
10317
|
+
);
|
|
10318
|
+
} else {
|
|
10319
|
+
ui.error(error instanceof Error ? error.message : "DLP transfer failed");
|
|
10320
|
+
}
|
|
10321
|
+
process.exitCode = 1;
|
|
10322
|
+
}
|
|
10323
|
+
function renderRestore(value, columns, format) {
|
|
10324
|
+
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10325
|
+
return formatOutput([value], columns, format);
|
|
10326
|
+
const markdown = format === "markdown";
|
|
10327
|
+
const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
|
|
10328
|
+
const heading = (text2) => markdown ? `### ${text2}
|
|
10329
|
+
` : text2;
|
|
10330
|
+
const blocks = [
|
|
10331
|
+
heading(
|
|
10332
|
+
value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
|
|
10333
|
+
),
|
|
10334
|
+
`Source TSG: ${value.sourceTsgId}
|
|
10335
|
+
${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
10336
|
+
];
|
|
10337
|
+
for (const [key, title] of [
|
|
10338
|
+
["dictionaries", "Dictionaries"],
|
|
10339
|
+
["patterns", "Data patterns"],
|
|
10340
|
+
["profiles", "Data profiles"]
|
|
10341
|
+
]) {
|
|
10342
|
+
blocks.push(heading(`${title} (${rows(key).length})`));
|
|
10343
|
+
blocks.push(
|
|
10344
|
+
formatOutput(
|
|
10345
|
+
rows(key),
|
|
10346
|
+
[
|
|
10347
|
+
{ key: "action", label: "Action" },
|
|
10348
|
+
{ key: "name", label: "Name" }
|
|
10349
|
+
],
|
|
10350
|
+
markdown ? "markdown" : "table"
|
|
10351
|
+
) || "(none)"
|
|
10352
|
+
);
|
|
10353
|
+
}
|
|
10354
|
+
if (rows("serverAdded").length) {
|
|
10355
|
+
blocks.push(heading("Verified server-added fields (source omitted these)"));
|
|
10356
|
+
for (const item of rows("serverAdded"))
|
|
10357
|
+
blocks.push(
|
|
10358
|
+
`${item.resource}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
|
|
10359
|
+
);
|
|
10360
|
+
}
|
|
10361
|
+
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10362
|
+
return blocks.join("\n\n");
|
|
10363
|
+
}
|
|
10364
|
+
function register6(dlp) {
|
|
10365
|
+
const backup = dlp.command("backup").description(
|
|
10366
|
+
"Back up custom DLP dictionaries (with keywords), data patterns and data profiles with their referenced dependencies"
|
|
10367
|
+
).option(
|
|
10368
|
+
"--resources <list>",
|
|
10369
|
+
"Comma-separated primary resources: dictionaries, patterns, profiles (default: all)",
|
|
10370
|
+
resourceKinds
|
|
10371
|
+
).option("--file-format <format>", "Backup file format: json or yaml", "json").option("--output-file <path>", "New private backup file; defaults to the current directory").option(
|
|
10372
|
+
"--skip-unsupported",
|
|
10373
|
+
"Exclude profiles with non-transferable rules (multi-profile, direct EDM) instead of failing"
|
|
10374
|
+
).option(
|
|
10375
|
+
"--max-pages <n>",
|
|
10376
|
+
"Maximum pages per inventory; incomplete exports fail",
|
|
10377
|
+
maxPages,
|
|
10378
|
+
100
|
|
10379
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10380
|
+
"after",
|
|
10381
|
+
examples(
|
|
10382
|
+
"airs runtime dlp backup --output-file ./dlp-backup.json",
|
|
10383
|
+
"airs runtime dlp backup --resources profiles --skip-unsupported --output-file ./profiles.yaml --file-format yaml"
|
|
10384
|
+
)
|
|
10385
|
+
).action(async (opts) => {
|
|
10386
|
+
try {
|
|
10387
|
+
if (!["json", "yaml"].includes(opts.fileFormat))
|
|
10388
|
+
throw new CliUsageError("--file-format must be json or yaml");
|
|
10389
|
+
const format = await resolveOutput(backup, opts);
|
|
10390
|
+
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10391
|
+
if (opts.outputFile === "-")
|
|
10392
|
+
throw new CliUsageError(
|
|
10393
|
+
"Backups contain dictionary keywords and require a private file destination, not stdout"
|
|
10394
|
+
);
|
|
10395
|
+
const path3 = resolve5(
|
|
10396
|
+
opts.outputFile ?? `airs-dlp-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
|
|
10397
|
+
);
|
|
10398
|
+
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10399
|
+
extname(path3).toLowerCase()
|
|
10400
|
+
))
|
|
10401
|
+
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10402
|
+
const { api, tsgId } = await context();
|
|
10403
|
+
const { backup: data, skipped } = await backupDlpResources(api, tsgId, {
|
|
10404
|
+
resources: opts.resources,
|
|
10405
|
+
maxPages: opts.maxPages,
|
|
10406
|
+
skipUnsupported: Boolean(opts.skipUnsupported)
|
|
10407
|
+
});
|
|
10408
|
+
for (const item of skipped)
|
|
10409
|
+
ui.warning(`Skipped unsupported data profile: ${item.profile} (${item.reason})`);
|
|
10410
|
+
const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10411
|
+
`;
|
|
10412
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
|
|
10413
|
+
throw new Error("Backup exceeds the 20 MiB size limit; select fewer --resources");
|
|
10414
|
+
await writeReportFile(path3, content);
|
|
10415
|
+
console.log(
|
|
10416
|
+
formatOutput(
|
|
10417
|
+
[
|
|
10418
|
+
{
|
|
10419
|
+
file: path3,
|
|
10420
|
+
sourceTsgId: tsgId,
|
|
10421
|
+
dictionaries: data.dictionaries.length,
|
|
10422
|
+
patterns: data.patterns.length,
|
|
10423
|
+
profiles: data.profiles.length,
|
|
10424
|
+
skipped: skipped.length
|
|
10425
|
+
}
|
|
10426
|
+
],
|
|
10427
|
+
[
|
|
10428
|
+
{ key: "file", label: "Backup file" },
|
|
10429
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10430
|
+
{ key: "dictionaries", label: "Dictionaries" },
|
|
10431
|
+
{ key: "patterns", label: "Patterns" },
|
|
10432
|
+
{ key: "profiles", label: "Profiles" },
|
|
10433
|
+
{ key: "skipped", label: "Skipped" }
|
|
10434
|
+
],
|
|
10435
|
+
format === "pretty" ? "table" : format
|
|
10436
|
+
)
|
|
10437
|
+
);
|
|
10438
|
+
} catch (error) {
|
|
10439
|
+
failure(error);
|
|
10440
|
+
}
|
|
10441
|
+
});
|
|
10442
|
+
const restore = dlp.command("restore <file>").description(
|
|
10443
|
+
"Restore DLP resources into the selected tenant, staged dictionaries \u2192 patterns \u2192 profiles; validate all dependencies before changes"
|
|
10444
|
+
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored custom resource names to avoid collisions").option(
|
|
10445
|
+
"--on-conflict <policy>",
|
|
10446
|
+
"Existing data profile names: error, verify (resume without writes) or skip. Updates are unsupported until the DLP profile write path is live-verified.",
|
|
10447
|
+
"error"
|
|
10448
|
+
).option(
|
|
10449
|
+
"--pattern-map <source=destination>",
|
|
10450
|
+
"Bind a tenant-bound source pattern to an existing destination pattern (repeatable)",
|
|
10451
|
+
(value, prior) => [...prior, value],
|
|
10452
|
+
[]
|
|
10453
|
+
).option(
|
|
10454
|
+
"--max-pages <n>",
|
|
10455
|
+
"Maximum pages per inventory; incomplete inventories fail",
|
|
10456
|
+
maxPages,
|
|
10457
|
+
100
|
|
10458
|
+
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10459
|
+
"--force",
|
|
10460
|
+
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10461
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10462
|
+
"after",
|
|
10463
|
+
examples(
|
|
10464
|
+
"airs runtime dlp restore ./dlp-backup.json --dry-run --output json",
|
|
10465
|
+
'airs runtime dlp restore ./dlp-backup.json --pattern-map "EDM SSN=Dest SSN" --dry-run',
|
|
10466
|
+
"airs runtime dlp restore ./dlp-backup.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10467
|
+
)
|
|
10468
|
+
).action(async (file, opts) => {
|
|
10469
|
+
try {
|
|
10470
|
+
if (!["error", "verify", "skip"].includes(opts.onConflict))
|
|
10471
|
+
throw new CliUsageError("--on-conflict must be error, verify or skip");
|
|
10472
|
+
if (opts.force && !opts.expectTsg)
|
|
10473
|
+
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10474
|
+
const mappings = patternMap(opts.patternMap);
|
|
10475
|
+
const path3 = resolve5(file);
|
|
10476
|
+
const extension = extname(path3).toLowerCase();
|
|
10477
|
+
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10478
|
+
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10479
|
+
const info = await stat(path3);
|
|
10480
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10481
|
+
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10482
|
+
let input2;
|
|
10483
|
+
try {
|
|
10484
|
+
const text2 = await readFile9(path3, "utf8");
|
|
10485
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10486
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10487
|
+
} catch {
|
|
10488
|
+
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10489
|
+
}
|
|
10490
|
+
const data = DlpResourcesBackupSchema.parse(input2);
|
|
10491
|
+
const format = await resolveOutput(restore, opts);
|
|
10492
|
+
const { api, tsgId } = await context();
|
|
10493
|
+
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10494
|
+
throw new CliUsageError(
|
|
10495
|
+
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
10496
|
+
);
|
|
10497
|
+
const plan = await planDlpResourcesRestore(api, data, tsgId, {
|
|
10498
|
+
namePrefix: opts.namePrefix,
|
|
10499
|
+
onConflict: opts.onConflict,
|
|
10500
|
+
patternMap: mappings,
|
|
10501
|
+
maxPages: opts.maxPages
|
|
10502
|
+
});
|
|
10503
|
+
const summary = {
|
|
10504
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10505
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10506
|
+
dryRun: Boolean(opts.dryRun),
|
|
10507
|
+
dictionaries: plan.dictionaries.map((d) => ({ name: d.name, action: d.action })),
|
|
10508
|
+
patterns: plan.patterns.map((p) => ({ name: p.name, action: p.action })),
|
|
10509
|
+
profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action }))
|
|
10510
|
+
};
|
|
10511
|
+
const columns = [
|
|
10512
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10513
|
+
{ key: "destinationTsgId", label: "Destination TSG" },
|
|
10514
|
+
{ key: "dictionaries", label: "Dictionaries" },
|
|
10515
|
+
{ key: "patterns", label: "Data patterns" },
|
|
10516
|
+
{ key: "profiles", label: "Data profiles" },
|
|
10517
|
+
{ key: "serverAdded", label: "Verified server-added fields" },
|
|
10518
|
+
{ key: "dryRun", label: "Dry run" },
|
|
10519
|
+
{ key: "complete", label: "Complete" }
|
|
10520
|
+
];
|
|
10521
|
+
if (opts.dryRun) {
|
|
10522
|
+
console.log(renderRestore(summary, columns, format));
|
|
10523
|
+
return;
|
|
10524
|
+
}
|
|
10525
|
+
const writes = plan.dictionaries.filter((d) => d.action === "create").length + plan.patterns.filter((p) => p.action === "create").length + plan.profiles.filter((p) => p.action === "create").length;
|
|
10526
|
+
if (writes)
|
|
10527
|
+
await confirmOrAbort(
|
|
10528
|
+
`Restore ${writes} DLP resources from TSG ${plan.sourceTsgId} to TSG ${tsgId}?`,
|
|
10529
|
+
Boolean(opts.force),
|
|
10530
|
+
{ action: `restore DLP resources into TSG ${tsgId}` }
|
|
10531
|
+
);
|
|
10532
|
+
const result = await restoreDlpResources(api, plan);
|
|
10533
|
+
console.log(renderRestore({ ...result }, columns, format));
|
|
10534
|
+
if (!result.complete) {
|
|
10535
|
+
ui.error(result.error ?? "Restore incomplete");
|
|
10536
|
+
process.exitCode = 1;
|
|
10537
|
+
}
|
|
10538
|
+
} catch (error) {
|
|
10539
|
+
failure(error);
|
|
10540
|
+
}
|
|
10541
|
+
});
|
|
10542
|
+
}
|
|
10543
|
+
|
|
9553
10544
|
// src/cli/commands/dlp/index.ts
|
|
9554
10545
|
function registerDlpCommands(runtime) {
|
|
9555
|
-
const dlp = runtime.command("dlp").description(
|
|
10546
|
+
const dlp = runtime.command("dlp").description(
|
|
10547
|
+
"DLP management (filtering-profiles, patterns, profiles, dictionaries, generate, backup/restore)"
|
|
10548
|
+
);
|
|
9556
10549
|
register2(dlp);
|
|
9557
10550
|
register4(dlp);
|
|
9558
10551
|
register5(dlp);
|
|
9559
10552
|
register(dlp);
|
|
9560
10553
|
register3(dlp);
|
|
10554
|
+
register6(dlp);
|
|
9561
10555
|
}
|
|
9562
10556
|
|
|
9563
10557
|
// src/cli/commands/profile-transfer.ts
|
|
9564
|
-
import { randomUUID as
|
|
9565
|
-
import { readFile as
|
|
9566
|
-
import { extname, resolve as
|
|
9567
|
-
import { AISecSDKException as
|
|
9568
|
-
import { dump as
|
|
9569
|
-
import { ZodError } from "zod";
|
|
10558
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10559
|
+
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
10560
|
+
import { extname as extname2, resolve as resolve6 } from "path";
|
|
10561
|
+
import { AISecSDKException as AISecSDKException3, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
10562
|
+
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
10563
|
+
import { ZodError as ZodError2 } from "zod";
|
|
9570
10564
|
|
|
9571
10565
|
// src/backup/runtime-profiles.ts
|
|
9572
10566
|
import {
|
|
@@ -9575,10 +10569,10 @@ import {
|
|
|
9575
10569
|
PolicySchema,
|
|
9576
10570
|
SecurityProfileSchema
|
|
9577
10571
|
} from "@cdot65/prisma-airs-sdk";
|
|
9578
|
-
import { z as
|
|
10572
|
+
import { z as z6 } from "zod";
|
|
9579
10573
|
|
|
9580
10574
|
// src/backup/runtime-policy.ts
|
|
9581
|
-
function
|
|
10575
|
+
function object2(value) {
|
|
9582
10576
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9583
10577
|
}
|
|
9584
10578
|
function compareRuntimePolicies(expected, actual) {
|
|
@@ -9599,7 +10593,7 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9599
10593
|
});
|
|
9600
10594
|
return;
|
|
9601
10595
|
}
|
|
9602
|
-
if (
|
|
10596
|
+
if (object2(source) && object2(target)) {
|
|
9603
10597
|
for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
|
|
9604
10598
|
const field = `${path3}.${key}`;
|
|
9605
10599
|
if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
|
|
@@ -9630,14 +10624,14 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9630
10624
|
}
|
|
9631
10625
|
if (relative === "model.app-protection" && key === "url-detected-severity")
|
|
9632
10626
|
allowed = "low";
|
|
9633
|
-
if (relative === "model.app-protection" && key === "default-url-category" &&
|
|
10627
|
+
if (relative === "model.app-protection" && key === "default-url-category" && object2(target[key])) {
|
|
9634
10628
|
const value = target[key];
|
|
9635
10629
|
if (Object.keys(value).length === 1 && value.member === null) {
|
|
9636
10630
|
serverDefaults.push(field);
|
|
9637
10631
|
continue;
|
|
9638
10632
|
}
|
|
9639
10633
|
}
|
|
9640
|
-
if ((/^model\.model-protection\[\d+\]$/.test(relative) && source.name === "toxic-content" && target.name === source.name || insideToxicCategories && /^model\.model-protection\[\d+\]\.toxic-category-list\[\d+\]$/.test(relative) && typeof source.category === "string" && source.category.length > 0 && source.category === target.category) && key === "severity-by-confidence" &&
|
|
10634
|
+
if ((/^model\.model-protection\[\d+\]$/.test(relative) && source.name === "toxic-content" && target.name === source.name || insideToxicCategories && /^model\.model-protection\[\d+\]\.toxic-category-list\[\d+\]$/.test(relative) && typeof source.category === "string" && source.category.length > 0 && source.category === target.category) && key === "severity-by-confidence" && object2(target[key])) {
|
|
9641
10635
|
const value = target[key];
|
|
9642
10636
|
if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
|
|
9643
10637
|
serverDefaults.push(field);
|
|
@@ -9672,26 +10666,26 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9672
10666
|
}
|
|
9673
10667
|
|
|
9674
10668
|
// src/backup/runtime-profiles.ts
|
|
9675
|
-
var RuntimeProfilesBackupSchema =
|
|
9676
|
-
version:
|
|
9677
|
-
resourceType:
|
|
9678
|
-
exportedAt:
|
|
9679
|
-
source:
|
|
9680
|
-
profiles:
|
|
9681
|
-
topics:
|
|
10669
|
+
var RuntimeProfilesBackupSchema = z6.object({
|
|
10670
|
+
version: z6.literal(1),
|
|
10671
|
+
resourceType: z6.literal("runtime-security-profiles"),
|
|
10672
|
+
exportedAt: z6.string().datetime({ offset: true }),
|
|
10673
|
+
source: z6.object({ tsgId: z6.string().min(1) }).strict(),
|
|
10674
|
+
profiles: z6.array(SecurityProfileSchema).min(1).max(1e4),
|
|
10675
|
+
topics: z6.array(CustomTopicSchema).max(1e4)
|
|
9682
10676
|
}).strict();
|
|
9683
|
-
function
|
|
10677
|
+
function object3(value) {
|
|
9684
10678
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9685
10679
|
}
|
|
9686
|
-
function
|
|
10680
|
+
function pagesLimit2(value) {
|
|
9687
10681
|
if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
|
|
9688
10682
|
throw new Error("maxPages must be an integer from 1 to 1000");
|
|
9689
10683
|
}
|
|
9690
|
-
async function offsetPages(fetch,
|
|
9691
|
-
|
|
10684
|
+
async function offsetPages(fetch, maxPages3) {
|
|
10685
|
+
pagesLimit2(maxPages3);
|
|
9692
10686
|
const items = [];
|
|
9693
10687
|
let offset = 0;
|
|
9694
|
-
for (let page = 0; page <
|
|
10688
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
9695
10689
|
const result = await fetch(offset);
|
|
9696
10690
|
items.push(...result.items);
|
|
9697
10691
|
if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
|
|
@@ -9706,7 +10700,7 @@ function latestProfiles(profiles2) {
|
|
|
9706
10700
|
const names = /* @__PURE__ */ new Map();
|
|
9707
10701
|
const revisions = /* @__PURE__ */ new Set();
|
|
9708
10702
|
for (const profile of profiles2) {
|
|
9709
|
-
|
|
10703
|
+
assertName2(profile.profile_name);
|
|
9710
10704
|
const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
|
|
9711
10705
|
if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
|
|
9712
10706
|
revisions.add(identity2);
|
|
@@ -9718,7 +10712,7 @@ function latestProfiles(profiles2) {
|
|
|
9718
10712
|
}
|
|
9719
10713
|
return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
|
|
9720
10714
|
}
|
|
9721
|
-
function
|
|
10715
|
+
function assertName2(name) {
|
|
9722
10716
|
if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9723
10717
|
throw new Error("Profile/topic names must be nonempty and contain no control characters");
|
|
9724
10718
|
}
|
|
@@ -9726,19 +10720,19 @@ function assertTenant(profiles2, tsgId) {
|
|
|
9726
10720
|
if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
|
|
9727
10721
|
throw new Error("Destination/source profile tenant identity does not match the selected TSG");
|
|
9728
10722
|
}
|
|
9729
|
-
async function profiles(api,
|
|
10723
|
+
async function profiles(api, maxPages3) {
|
|
9730
10724
|
return latestProfiles(
|
|
9731
10725
|
await offsetPages(async (offset) => {
|
|
9732
10726
|
const page = await api.profiles.list({ offset, limit: 100, latest: true });
|
|
9733
10727
|
return { items: page.ai_profiles, next: page.next_offset };
|
|
9734
|
-
},
|
|
10728
|
+
}, maxPages3)
|
|
9735
10729
|
);
|
|
9736
10730
|
}
|
|
9737
|
-
async function topics(api,
|
|
10731
|
+
async function topics(api, maxPages3) {
|
|
9738
10732
|
return offsetPages(async (offset) => {
|
|
9739
10733
|
const page = await api.topics.list({ offset, limit: 100 });
|
|
9740
10734
|
return { items: page.custom_topics, next: page.next_offset };
|
|
9741
|
-
},
|
|
10735
|
+
}, maxPages3);
|
|
9742
10736
|
}
|
|
9743
10737
|
function topicReferences(policy) {
|
|
9744
10738
|
const refs = [];
|
|
@@ -9747,7 +10741,7 @@ function topicReferences(policy) {
|
|
|
9747
10741
|
value.forEach(visit);
|
|
9748
10742
|
return;
|
|
9749
10743
|
}
|
|
9750
|
-
if (!
|
|
10744
|
+
if (!object3(value)) return;
|
|
9751
10745
|
if ("topic_id" in value) {
|
|
9752
10746
|
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9753
10747
|
throw new Error("Invalid topic identity in profile policy");
|
|
@@ -9763,8 +10757,8 @@ function topicKey(ref) {
|
|
|
9763
10757
|
}
|
|
9764
10758
|
async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
9765
10759
|
if (!tsgId) throw new Error("Source TSG ID is required");
|
|
9766
|
-
const
|
|
9767
|
-
let selected = await profiles(api,
|
|
10760
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
10761
|
+
let selected = await profiles(api, maxPages3);
|
|
9768
10762
|
assertTenant(selected, tsgId);
|
|
9769
10763
|
if (options.profile)
|
|
9770
10764
|
selected = selected.filter(
|
|
@@ -9777,7 +10771,7 @@ async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
|
9777
10771
|
PolicySchema.parse(profile.policy);
|
|
9778
10772
|
}
|
|
9779
10773
|
const refs = selected.flatMap((profile) => topicReferences(profile.policy));
|
|
9780
|
-
const definitions = refs.length ? await topics(api,
|
|
10774
|
+
const definitions = refs.length ? await topics(api, maxPages3) : [];
|
|
9781
10775
|
const needed = /* @__PURE__ */ new Map();
|
|
9782
10776
|
for (const ref of refs) {
|
|
9783
10777
|
const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
|
|
@@ -9800,21 +10794,21 @@ function sameTopic(a, b) {
|
|
|
9800
10794
|
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9801
10795
|
}
|
|
9802
10796
|
function dlpReferences(policy) {
|
|
9803
|
-
if (!
|
|
10797
|
+
if (!object3(policy)) return [];
|
|
9804
10798
|
const refs = [];
|
|
9805
10799
|
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9806
|
-
if (!
|
|
10800
|
+
if (!object3(entry) || typeof entry.name !== "string")
|
|
9807
10801
|
throw new Error("Invalid embedded DLP reference");
|
|
9808
10802
|
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9809
10803
|
}
|
|
9810
10804
|
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9811
|
-
if (!
|
|
10805
|
+
if (!object3(entry) || !object3(entry["model-configuration"])) continue;
|
|
9812
10806
|
const protection = entry["model-configuration"]["data-protection"];
|
|
9813
|
-
if (!
|
|
10807
|
+
if (!object3(protection) || !object3(protection["data-leak-detection"])) continue;
|
|
9814
10808
|
const members = protection["data-leak-detection"].member;
|
|
9815
10809
|
if (!Array.isArray(members)) continue;
|
|
9816
10810
|
for (const member of members) {
|
|
9817
|
-
if (!
|
|
10811
|
+
if (!object3(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9818
10812
|
if (member.text.toLowerCase() === "sensitive content" && (member.id === "" || member.id === void 0) && (member.version === "2" || member.version === void 0) && !refs.some((ref) => ref.kind === "embedded" && ref.name === member.text))
|
|
9819
10813
|
continue;
|
|
9820
10814
|
refs.push({ value: member, name: member.text, kind: "member" });
|
|
@@ -9857,7 +10851,7 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9857
10851
|
});
|
|
9858
10852
|
return;
|
|
9859
10853
|
}
|
|
9860
|
-
if (!
|
|
10854
|
+
if (!object3(value)) return;
|
|
9861
10855
|
for (const [key, child] of Object.entries(value)) {
|
|
9862
10856
|
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9863
10857
|
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
@@ -9866,9 +10860,9 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9866
10860
|
};
|
|
9867
10861
|
visit(policy, "policy");
|
|
9868
10862
|
}
|
|
9869
|
-
async function dataProfiles(api,
|
|
10863
|
+
async function dataProfiles(api, maxPages3) {
|
|
9870
10864
|
const items = [];
|
|
9871
|
-
for (let page = 0; page <
|
|
10865
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
9872
10866
|
const result = await api.dataProfiles.list({ page, size: 100 });
|
|
9873
10867
|
items.push(...result.content);
|
|
9874
10868
|
if (items.length > 1e4) throw new Error("DLP inventory exceeds the safety limit");
|
|
@@ -9881,8 +10875,8 @@ async function dataProfiles(api, maxPages2) {
|
|
|
9881
10875
|
async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9882
10876
|
const backup = RuntimeProfilesBackupSchema.parse(input2);
|
|
9883
10877
|
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9884
|
-
const
|
|
9885
|
-
|
|
10878
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
10879
|
+
pagesLimit2(maxPages3);
|
|
9886
10880
|
const prefix = options.namePrefix ?? "";
|
|
9887
10881
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9888
10882
|
throw new Error("Invalid --name-prefix");
|
|
@@ -9905,7 +10899,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9905
10899
|
});
|
|
9906
10900
|
if (crossTenant) assertKnownPolicyReferences(profile.policy);
|
|
9907
10901
|
}
|
|
9908
|
-
const existing = await profiles(api,
|
|
10902
|
+
const existing = await profiles(api, maxPages3);
|
|
9909
10903
|
assertTenant(existing, destinationTsgId);
|
|
9910
10904
|
const profilePlans = selected.map((source) => {
|
|
9911
10905
|
const name = `${prefix}${source.profile_name}`;
|
|
@@ -9924,7 +10918,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9924
10918
|
});
|
|
9925
10919
|
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
9926
10920
|
const refs = changing.flatMap((p) => topicReferences(p.source.policy));
|
|
9927
|
-
const targetTopics = refs.length ? await topics(api,
|
|
10921
|
+
const targetTopics = refs.length ? await topics(api, maxPages3) : [];
|
|
9928
10922
|
const topicPlans = /* @__PURE__ */ new Map();
|
|
9929
10923
|
const topicNames = /* @__PURE__ */ new Map();
|
|
9930
10924
|
for (const ref of refs) {
|
|
@@ -9937,7 +10931,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9937
10931
|
throw new Error(`Backup lacks an unambiguous topic definition: ${ref.topic_name}`);
|
|
9938
10932
|
const source = definitions[0];
|
|
9939
10933
|
const name = `${prefix}${source.topic_name}`;
|
|
9940
|
-
|
|
10934
|
+
assertName2(name);
|
|
9941
10935
|
if (topicNames.has(name) && topicNames.get(name) !== key)
|
|
9942
10936
|
throw new Error(
|
|
9943
10937
|
`Multiple source revisions use topic name ${name}; split the backup by profile/revision before restoring`
|
|
@@ -9964,7 +10958,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9964
10958
|
);
|
|
9965
10959
|
if (!targetFor(name)) unresolved.add(name);
|
|
9966
10960
|
}
|
|
9967
|
-
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api,
|
|
10961
|
+
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages3) : [];
|
|
9968
10962
|
const resolved = [];
|
|
9969
10963
|
for (const name of names) {
|
|
9970
10964
|
const target = targetFor(name);
|
|
@@ -10015,7 +11009,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
10015
11009
|
dlpMappings,
|
|
10016
11010
|
dlpFallbacks,
|
|
10017
11011
|
serverDefaults: [],
|
|
10018
|
-
maxPages:
|
|
11012
|
+
maxPages: maxPages3
|
|
10019
11013
|
};
|
|
10020
11014
|
const existingTopics = new Map(
|
|
10021
11015
|
plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
|
|
@@ -10032,8 +11026,8 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
10032
11026
|
}
|
|
10033
11027
|
return plan;
|
|
10034
11028
|
}
|
|
10035
|
-
function
|
|
10036
|
-
const order = (v) => Array.isArray(v) ? v.map(order) :
|
|
11029
|
+
function canonical2(value) {
|
|
11030
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object3(v) ? Object.fromEntries(
|
|
10037
11031
|
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
10038
11032
|
) : v;
|
|
10039
11033
|
return JSON.stringify(order(value));
|
|
@@ -10044,7 +11038,7 @@ function canonicalPolicy(policy) {
|
|
|
10044
11038
|
const protection = entry["model-configuration"]?.["data-protection"];
|
|
10045
11039
|
if (protection && !("database-security" in protection)) protection["database-security"] = null;
|
|
10046
11040
|
}
|
|
10047
|
-
return
|
|
11041
|
+
return canonical2(value);
|
|
10048
11042
|
}
|
|
10049
11043
|
function unchanged(current, prior) {
|
|
10050
11044
|
return Boolean(
|
|
@@ -10187,9 +11181,9 @@ function mappedPolicy(item, plan, mappedTopics) {
|
|
|
10187
11181
|
}
|
|
10188
11182
|
|
|
10189
11183
|
// src/cli/commands/profile-transfer.ts
|
|
10190
|
-
var
|
|
10191
|
-
var
|
|
10192
|
-
function
|
|
11184
|
+
var MAX_BACKUP_BYTES2 = 20 * 1024 * 1024;
|
|
11185
|
+
var FORMATS2 = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
11186
|
+
function renderRestore2(value, columns, format) {
|
|
10193
11187
|
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10194
11188
|
return formatOutput([value], columns, format);
|
|
10195
11189
|
const markdown = format === "markdown";
|
|
@@ -10249,7 +11243,7 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
|
10249
11243
|
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10250
11244
|
return blocks.join("\n\n");
|
|
10251
11245
|
}
|
|
10252
|
-
function
|
|
11246
|
+
function maxPages2(value) {
|
|
10253
11247
|
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
10254
11248
|
usageError("--max-pages must be an integer from 1 to 1000");
|
|
10255
11249
|
return Number(value);
|
|
@@ -10265,13 +11259,13 @@ function dlpMap(values) {
|
|
|
10265
11259
|
throw new CliUsageError("Duplicate source names in --dlp-map");
|
|
10266
11260
|
return Object.fromEntries(entries);
|
|
10267
11261
|
}
|
|
10268
|
-
async function
|
|
11262
|
+
async function context2() {
|
|
10269
11263
|
const config = await loadConfig();
|
|
10270
11264
|
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
10271
11265
|
throw new CliUsageError(
|
|
10272
11266
|
"Profile transfer requires configured Management client ID, secret and TSG ID"
|
|
10273
11267
|
);
|
|
10274
|
-
const client = new
|
|
11268
|
+
const client = new ManagementClient2({ ...managementClientOptions(config), numRetries: 0 });
|
|
10275
11269
|
return {
|
|
10276
11270
|
api: {
|
|
10277
11271
|
profiles: client.profiles,
|
|
@@ -10281,11 +11275,11 @@ async function context() {
|
|
|
10281
11275
|
tsgId: config.mgmtTsgId
|
|
10282
11276
|
};
|
|
10283
11277
|
}
|
|
10284
|
-
function
|
|
11278
|
+
function failure2(error) {
|
|
10285
11279
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
10286
|
-
if (error instanceof
|
|
11280
|
+
if (error instanceof ZodError2) {
|
|
10287
11281
|
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
10288
|
-
} else if (error instanceof
|
|
11282
|
+
} else if (error instanceof AISecSDKException3) {
|
|
10289
11283
|
ui.error(
|
|
10290
11284
|
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10291
11285
|
);
|
|
@@ -10304,9 +11298,9 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10304
11298
|
).option("--all", "Back up all latest profiles (cannot be combined with a profile selector)").option("--file-format <format>", "Backup file format: json or yaml", "json").option("--output-file <path>", "New private backup file; defaults to the current directory").option(
|
|
10305
11299
|
"--max-pages <n>",
|
|
10306
11300
|
"Maximum pages per inventory; incomplete exports fail",
|
|
10307
|
-
|
|
11301
|
+
maxPages2,
|
|
10308
11302
|
100
|
|
10309
|
-
).option("--output <format>",
|
|
11303
|
+
).option("--output <format>", FORMATS2).addHelpText(
|
|
10310
11304
|
"after",
|
|
10311
11305
|
examples(
|
|
10312
11306
|
"airs runtime profiles backup --all --output-file ./profiles.json",
|
|
@@ -10322,18 +11316,18 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10322
11316
|
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10323
11317
|
if (opts.outputFile === "-")
|
|
10324
11318
|
throw new CliUsageError("Backups require a private file destination, not stdout");
|
|
10325
|
-
const path3 =
|
|
10326
|
-
opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${
|
|
11319
|
+
const path3 = resolve6(
|
|
11320
|
+
opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID8().slice(0, 8)}${extension}`
|
|
10327
11321
|
);
|
|
10328
11322
|
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10329
|
-
|
|
11323
|
+
extname2(path3).toLowerCase()
|
|
10330
11324
|
))
|
|
10331
11325
|
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10332
|
-
const { api, tsgId } = await
|
|
11326
|
+
const { api, tsgId } = await context2();
|
|
10333
11327
|
const data = await backupRuntimeProfiles(api, tsgId, { profile, maxPages: opts.maxPages });
|
|
10334
|
-
const content = opts.fileFormat === "yaml" ?
|
|
11328
|
+
const content = opts.fileFormat === "yaml" ? dump7(data, { schema: JSON_SCHEMA2, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10335
11329
|
`;
|
|
10336
|
-
if (Buffer.byteLength(content) >
|
|
11330
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES2)
|
|
10337
11331
|
throw new Error("Backup exceeds the 20 MiB size limit; select individual profiles");
|
|
10338
11332
|
await writeReportFile(path3, content);
|
|
10339
11333
|
console.log(
|
|
@@ -10356,7 +11350,7 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10356
11350
|
)
|
|
10357
11351
|
);
|
|
10358
11352
|
} catch (error) {
|
|
10359
|
-
|
|
11353
|
+
failure2(error);
|
|
10360
11354
|
}
|
|
10361
11355
|
});
|
|
10362
11356
|
const restore = profiles2.command("restore <file>").description(
|
|
@@ -10377,12 +11371,12 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10377
11371
|
).option(
|
|
10378
11372
|
"--max-pages <n>",
|
|
10379
11373
|
"Maximum pages per inventory; incomplete inventories fail",
|
|
10380
|
-
|
|
11374
|
+
maxPages2,
|
|
10381
11375
|
100
|
|
10382
11376
|
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10383
11377
|
"--force",
|
|
10384
11378
|
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10385
|
-
).option("--output <format>",
|
|
11379
|
+
).option("--output <format>", FORMATS2).addHelpText(
|
|
10386
11380
|
"after",
|
|
10387
11381
|
examples(
|
|
10388
11382
|
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
@@ -10398,24 +11392,24 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10398
11392
|
if (opts.force && !opts.expectTsg)
|
|
10399
11393
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10400
11394
|
const mappings = dlpMap(opts.dlpMap);
|
|
10401
|
-
const path3 =
|
|
10402
|
-
const extension =
|
|
11395
|
+
const path3 = resolve6(file);
|
|
11396
|
+
const extension = extname2(path3).toLowerCase();
|
|
10403
11397
|
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10404
11398
|
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10405
|
-
const info = await
|
|
10406
|
-
if (!info.isFile() || info.size >
|
|
11399
|
+
const info = await stat2(path3);
|
|
11400
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES2)
|
|
10407
11401
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10408
11402
|
let input2;
|
|
10409
11403
|
try {
|
|
10410
|
-
const text2 = await
|
|
10411
|
-
if (Buffer.byteLength(text2) >
|
|
10412
|
-
input2 = extension === ".json" ? JSON.parse(text2) :
|
|
11404
|
+
const text2 = await readFile10(path3, "utf8");
|
|
11405
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES2) throw new Error("Backup too large");
|
|
11406
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load3(text2, { schema: JSON_SCHEMA2 });
|
|
10413
11407
|
} catch {
|
|
10414
11408
|
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10415
11409
|
}
|
|
10416
11410
|
const data = RuntimeProfilesBackupSchema.parse(input2);
|
|
10417
11411
|
const format = await resolveOutput(restore, opts);
|
|
10418
|
-
const { api, tsgId } = await
|
|
11412
|
+
const { api, tsgId } = await context2();
|
|
10419
11413
|
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10420
11414
|
throw new CliUsageError(
|
|
10421
11415
|
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
@@ -10459,7 +11453,7 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10459
11453
|
`DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
|
|
10460
11454
|
);
|
|
10461
11455
|
if (opts.dryRun) {
|
|
10462
|
-
console.log(
|
|
11456
|
+
console.log(renderRestore2(summary, columns, format));
|
|
10463
11457
|
return;
|
|
10464
11458
|
}
|
|
10465
11459
|
const changing = plan.profiles.filter(
|
|
@@ -10472,13 +11466,13 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10472
11466
|
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10473
11467
|
);
|
|
10474
11468
|
const result = await restoreRuntimeProfiles(api, plan);
|
|
10475
|
-
console.log(
|
|
11469
|
+
console.log(renderRestore2({ ...result }, columns, format));
|
|
10476
11470
|
if (!result.complete) {
|
|
10477
11471
|
ui.error(result.error ?? "Restore incomplete");
|
|
10478
11472
|
process.exitCode = 1;
|
|
10479
11473
|
}
|
|
10480
11474
|
} catch (error) {
|
|
10481
|
-
|
|
11475
|
+
failure2(error);
|
|
10482
11476
|
}
|
|
10483
11477
|
});
|
|
10484
11478
|
}
|
|
@@ -10586,8 +11580,8 @@ function registerCleanupCommand(parent) {
|
|
|
10586
11580
|
|
|
10587
11581
|
// src/cli/commands/runtime-dashboard.ts
|
|
10588
11582
|
import { lstat as lstat4 } from "fs/promises";
|
|
10589
|
-
import { resolve as
|
|
10590
|
-
import { dump as
|
|
11583
|
+
import { resolve as resolve7 } from "path";
|
|
11584
|
+
import { dump as dump8 } from "js-yaml";
|
|
10591
11585
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
10592
11586
|
function integer3(value, name, minimum = 0) {
|
|
10593
11587
|
const n = Number(value);
|
|
@@ -10649,7 +11643,7 @@ function scanOptions(command) {
|
|
|
10649
11643
|
function registerRuntimeDashboardCommands(runtime) {
|
|
10650
11644
|
const dashboard = runtime.command("dashboard").description("SCM application summaries and daily telemetry (OAuth)");
|
|
10651
11645
|
const sessions = runtime.command("sessions").description("Verified SCM session inventory and drill-down (OAuth)");
|
|
10652
|
-
function
|
|
11646
|
+
function register7(command, prepare) {
|
|
10653
11647
|
command.option("--output <format>", "Output format: pretty, json, yaml").action(async (opts) => {
|
|
10654
11648
|
try {
|
|
10655
11649
|
const read = prepare(opts);
|
|
@@ -10662,7 +11656,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10662
11656
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
10663
11657
|
);
|
|
10664
11658
|
if (opts.outputFile) {
|
|
10665
|
-
const path3 =
|
|
11659
|
+
const path3 = resolve7(text(opts.outputFile, "--output-file"));
|
|
10666
11660
|
const existing = await lstat4(path3).catch((error) => {
|
|
10667
11661
|
if (error.code !== "ENOENT") throw error;
|
|
10668
11662
|
return void 0;
|
|
@@ -10676,9 +11670,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10676
11670
|
numRetries: 0
|
|
10677
11671
|
});
|
|
10678
11672
|
const result = await read(client.dashboard);
|
|
10679
|
-
const output = format === "yaml" ?
|
|
11673
|
+
const output = format === "yaml" ? dump8(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
10680
11674
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
10681
|
-
await writeReportFile(
|
|
11675
|
+
await writeReportFile(resolve7(String(opts.outputFile)), `${output}
|
|
10682
11676
|
`);
|
|
10683
11677
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
10684
11678
|
} else console.log(output);
|
|
@@ -10692,7 +11686,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10692
11686
|
}
|
|
10693
11687
|
});
|
|
10694
11688
|
}
|
|
10695
|
-
|
|
11689
|
+
register7(
|
|
10696
11690
|
pageOptions2(
|
|
10697
11691
|
timeOptions(
|
|
10698
11692
|
dashboard.command("applications").description("One page of application activity"),
|
|
@@ -10721,7 +11715,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10721
11715
|
["application", "application"],
|
|
10722
11716
|
["application-violations", "applicationViolationBreakdown"]
|
|
10723
11717
|
]) {
|
|
10724
|
-
|
|
11718
|
+
register7(
|
|
10725
11719
|
timeOptions(
|
|
10726
11720
|
dashboard.command(name).requiredOption("--app-id <id>", "Application identity").requiredOption("--app-name <name>", "Exact dashboard application name"),
|
|
10727
11721
|
"30",
|
|
@@ -10747,7 +11741,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10747
11741
|
["violations-trend", "applicationsViolationsTrend", false],
|
|
10748
11742
|
["apps-list", "appsList", true]
|
|
10749
11743
|
]) {
|
|
10750
|
-
|
|
11744
|
+
register7(
|
|
10751
11745
|
timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
|
|
10752
11746
|
(opts) => {
|
|
10753
11747
|
const query2 = window(opts);
|
|
@@ -10755,11 +11749,11 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10755
11749
|
}
|
|
10756
11750
|
);
|
|
10757
11751
|
}
|
|
10758
|
-
|
|
11752
|
+
register7(timeOptions(sessions.command("chart")), (opts) => {
|
|
10759
11753
|
const query2 = window(opts);
|
|
10760
11754
|
return (client) => client.sessionsChart(query2);
|
|
10761
11755
|
});
|
|
10762
|
-
|
|
11756
|
+
register7(
|
|
10763
11757
|
pageOptions2(
|
|
10764
11758
|
timeOptions(
|
|
10765
11759
|
sessions.command("list").description("Session entries; --all walks pages with completeness checks")
|
|
@@ -10825,7 +11819,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10825
11819
|
};
|
|
10826
11820
|
}
|
|
10827
11821
|
);
|
|
10828
|
-
|
|
11822
|
+
register7(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
|
|
10829
11823
|
const query2 = {
|
|
10830
11824
|
...identity(opts),
|
|
10831
11825
|
limit: integer3(opts.limit, "--limit", 1),
|
|
@@ -10833,11 +11827,11 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10833
11827
|
};
|
|
10834
11828
|
return (client) => client.session(query2);
|
|
10835
11829
|
});
|
|
10836
|
-
|
|
11830
|
+
register7(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
|
|
10837
11831
|
const query2 = { ...identity(opts), ...scan(opts) };
|
|
10838
11832
|
return (client) => client.sessionTransaction(query2);
|
|
10839
11833
|
});
|
|
10840
|
-
|
|
11834
|
+
register7(
|
|
10841
11835
|
scanOptions(
|
|
10842
11836
|
sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
|
|
10843
11837
|
).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
|
|
@@ -10850,8 +11844,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10850
11844
|
|
|
10851
11845
|
// src/cli/commands/runtime-report.ts
|
|
10852
11846
|
import { lstat as lstat5 } from "fs/promises";
|
|
10853
|
-
import { resolve as
|
|
10854
|
-
import { randomUUID as
|
|
11847
|
+
import { resolve as resolve8 } from "path";
|
|
11848
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
10855
11849
|
function registerRuntimeReportCommand(runtime) {
|
|
10856
11850
|
const command = runtime.command("report").description("Generate a daily AI Runtime Security environment report (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
|
|
10857
11851
|
"--output-file <path>",
|
|
@@ -10868,15 +11862,15 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10868
11862
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
10869
11863
|
if (format !== "html" && format !== "markdown")
|
|
10870
11864
|
usageError("Report output must be html or markdown");
|
|
10871
|
-
const
|
|
10872
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
11865
|
+
const maxPages3 = Number(opts.maxPages);
|
|
11866
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
|
|
10873
11867
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
10874
11868
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
10875
11869
|
usageError("--title must contain 1\u2013240 characters");
|
|
10876
11870
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
10877
11871
|
usageError("--output-file cannot be empty");
|
|
10878
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
10879
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
11872
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID9().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
11873
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve8(opts.outputFile ?? defaultName);
|
|
10880
11874
|
try {
|
|
10881
11875
|
if (outputPath) {
|
|
10882
11876
|
const existing = await lstat5(outputPath).catch((error) => {
|
|
@@ -10891,7 +11885,7 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10891
11885
|
numRetries: 0
|
|
10892
11886
|
});
|
|
10893
11887
|
ui.status("Collecting daily runtime activity and current configuration (read-only)...");
|
|
10894
|
-
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages:
|
|
11888
|
+
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages3 });
|
|
10895
11889
|
const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
|
|
10896
11890
|
if (outputPath) {
|
|
10897
11891
|
await writeReportFile(outputPath, rendered);
|
|
@@ -11051,7 +12045,7 @@ function registerCreateCommand(parent) {
|
|
|
11051
12045
|
}
|
|
11052
12046
|
|
|
11053
12047
|
// src/cli/commands/topics-eval.ts
|
|
11054
|
-
import { readFile as
|
|
12048
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
11055
12049
|
|
|
11056
12050
|
// src/core/prompt-loader.ts
|
|
11057
12051
|
function parseCsvLine(line) {
|
|
@@ -11194,7 +12188,7 @@ function registerEvalCommand(parent) {
|
|
|
11194
12188
|
resolveDeprecatedAliases(cmd, opts);
|
|
11195
12189
|
try {
|
|
11196
12190
|
const config = await loadConfig();
|
|
11197
|
-
const csvContent = await
|
|
12191
|
+
const csvContent = await readFile11(opts.prompts, "utf-8");
|
|
11198
12192
|
const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
|
|
11199
12193
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
11200
12194
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
@@ -11366,7 +12360,7 @@ function completedBulkResults(state) {
|
|
|
11366
12360
|
}
|
|
11367
12361
|
async function writeBulkResults(outputPath, results) {
|
|
11368
12362
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
11369
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
12363
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
|
|
11370
12364
|
try {
|
|
11371
12365
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
11372
12366
|
encoding: "utf-8",
|
|
@@ -11493,7 +12487,7 @@ function registerRuntimeCommand(program) {
|
|
|
11493
12487
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
11494
12488
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
11495
12489
|
}
|
|
11496
|
-
const raw = await
|
|
12490
|
+
const raw = await readFile12(opts.file, "utf-8");
|
|
11497
12491
|
const prompts = parseInputFile(raw, opts.file);
|
|
11498
12492
|
if (prompts.length === 0) {
|
|
11499
12493
|
usageError("No prompts found in input file");
|
|
@@ -12151,7 +13145,7 @@ function registerRuntimeCommand(program) {
|
|
|
12151
13145
|
}
|
|
12152
13146
|
|
|
12153
13147
|
// src/config/tenant-settings.ts
|
|
12154
|
-
import { randomUUID as
|
|
13148
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
12155
13149
|
import { constants } from "fs";
|
|
12156
13150
|
import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
12157
13151
|
import { dirname as dirname4, join as join4 } from "path";
|
|
@@ -12181,7 +13175,7 @@ async function createManagedTenant(name, config) {
|
|
|
12181
13175
|
throw new Error("Tenant name already exists");
|
|
12182
13176
|
const directory = join4(dirname4(tenantStorePath()), "configs");
|
|
12183
13177
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
12184
|
-
const path3 = join4(directory, `${name}-${
|
|
13178
|
+
const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
|
|
12185
13179
|
const file = await open2(path3, "wx", 384);
|
|
12186
13180
|
try {
|
|
12187
13181
|
const data = Object.fromEntries(
|
|
@@ -12211,11 +13205,11 @@ async function setTenantSetting(name, key, value) {
|
|
|
12211
13205
|
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
12212
13206
|
);
|
|
12213
13207
|
});
|
|
12214
|
-
const temporary = join4(dirname4(path3), `.airs-config-${
|
|
13208
|
+
const temporary = join4(dirname4(path3), `.airs-config-${randomUUID11()}.tmp`);
|
|
12215
13209
|
let created = false;
|
|
12216
13210
|
try {
|
|
12217
|
-
const
|
|
12218
|
-
if (!
|
|
13211
|
+
const stat3 = await lstat6(path3);
|
|
13212
|
+
if (!stat3.isFile() || (stat3.mode & 146) === 0)
|
|
12219
13213
|
throw new Error("Tenant config is not a writable regular file");
|
|
12220
13214
|
await access(path3, constants.W_OK);
|
|
12221
13215
|
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
@@ -12511,7 +13505,7 @@ function buildProgram() {
|
|
|
12511
13505
|
if (root.debug) {
|
|
12512
13506
|
const logPath = join5(
|
|
12513
13507
|
process.cwd(),
|
|
12514
|
-
`debug-api-${Date.now()}-${
|
|
13508
|
+
`debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
|
|
12515
13509
|
);
|
|
12516
13510
|
try {
|
|
12517
13511
|
let ancestor = actionCommand;
|