@cdot65/prisma-airs-cli 5.7.1 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1135 -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,1015 @@ 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
|
+
if (!options.skipUnsupported)
|
|
9774
|
+
throw new Error(`Profile cannot be exported: ${name} ${reason}`);
|
|
9775
|
+
skipped.push({ profile: name, reason });
|
|
9776
|
+
}
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
9779
|
+
const byName = (a, b) => (a.name ?? "").localeCompare(b.name ?? "");
|
|
9780
|
+
const dictionaryRecords = [...dictionaries.values()].sort(byName);
|
|
9781
|
+
const patternRecords = [...patterns.values()].sort(byName);
|
|
9782
|
+
profiles2.sort(byName);
|
|
9783
|
+
uniqueNames(dictionaryRecords, "dictionary");
|
|
9784
|
+
uniqueNames(patternRecords, "data pattern");
|
|
9785
|
+
uniqueNames(profiles2, "data profile");
|
|
9786
|
+
if (!dictionaryRecords.length && !patternRecords.length && !profiles2.length)
|
|
9787
|
+
throw new Error("No matching DLP resources to back up");
|
|
9788
|
+
const backup = DlpResourcesBackupSchema.parse({
|
|
9789
|
+
version: 1,
|
|
9790
|
+
resourceType: "dlp-resources",
|
|
9791
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9792
|
+
source: { tsgId },
|
|
9793
|
+
dictionaries: dictionaryRecords,
|
|
9794
|
+
patterns: patternRecords,
|
|
9795
|
+
profiles: profiles2
|
|
9796
|
+
});
|
|
9797
|
+
return { backup, skipped };
|
|
9798
|
+
}
|
|
9799
|
+
function compareEcho(request, readBack) {
|
|
9800
|
+
const differences = [];
|
|
9801
|
+
const serverAdded = [];
|
|
9802
|
+
const visit = (source, target, path3, top) => {
|
|
9803
|
+
const a = source ?? null;
|
|
9804
|
+
const b = target ?? null;
|
|
9805
|
+
if (a === null && b === null) return;
|
|
9806
|
+
if (a === null) {
|
|
9807
|
+
serverAdded.push(path3);
|
|
9808
|
+
return;
|
|
9809
|
+
}
|
|
9810
|
+
if (b === null) {
|
|
9811
|
+
differences.push(path3);
|
|
9812
|
+
return;
|
|
9813
|
+
}
|
|
9814
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
9815
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
9816
|
+
differences.push(path3);
|
|
9817
|
+
return;
|
|
9818
|
+
}
|
|
9819
|
+
a.forEach((item, index) => {
|
|
9820
|
+
visit(item, b[index], `${path3}[${index}]`, false);
|
|
9821
|
+
});
|
|
9822
|
+
return;
|
|
9823
|
+
}
|
|
9824
|
+
if (object(a) && object(b)) {
|
|
9825
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
9826
|
+
if (top && !(key in a) && SERVER_METADATA_KEYS.has(key)) continue;
|
|
9827
|
+
visit(a[key], b[key], `${path3}.${key}`, false);
|
|
9828
|
+
}
|
|
9829
|
+
return;
|
|
9830
|
+
}
|
|
9831
|
+
if (a !== b) differences.push(path3);
|
|
9832
|
+
};
|
|
9833
|
+
visit(request, readBack, "body", true);
|
|
9834
|
+
return { matches: differences.length === 0, differences, serverAdded };
|
|
9835
|
+
}
|
|
9836
|
+
function keywordSet(value) {
|
|
9837
|
+
if (!Array.isArray(value) || value.some((keyword) => typeof keyword !== "string"))
|
|
9838
|
+
return void 0;
|
|
9839
|
+
return [...value].sort();
|
|
9840
|
+
}
|
|
9841
|
+
function dictionaryEcho(metadata, record, keywords) {
|
|
9842
|
+
const { original_file_name, ...projection } = metadata;
|
|
9843
|
+
const echo = compareEcho(projection, record);
|
|
9844
|
+
const echoedFile = record?.dictionary_metadata?.original_file_name;
|
|
9845
|
+
const echoedKeywords = keywordSet(record?.keywords);
|
|
9846
|
+
const matches = echo.matches && (echoedFile == null || echoedFile === original_file_name) && echoedKeywords !== void 0 && canonical(echoedKeywords) === canonical(keywords);
|
|
9847
|
+
return { matches, serverAdded: echo.serverAdded };
|
|
9848
|
+
}
|
|
9849
|
+
function dictionaryRequest(source, name) {
|
|
9850
|
+
const metadata = {
|
|
9851
|
+
category: source.category,
|
|
9852
|
+
name,
|
|
9853
|
+
original_file_name: source.dictionary_metadata?.original_file_name ?? "keywords.txt",
|
|
9854
|
+
region_name: source.region_name,
|
|
9855
|
+
...source.description != null ? { description: source.description } : {},
|
|
9856
|
+
...source.is_case_sensitive != null ? { is_case_sensitive: source.is_case_sensitive } : {}
|
|
9857
|
+
};
|
|
9858
|
+
const parsed = DictionaryRequestSchema.safeParse(metadata);
|
|
9859
|
+
if (!parsed.success)
|
|
9860
|
+
throw new Error(`Dictionary cannot be rebuilt from the backup: ${source.name}`);
|
|
9861
|
+
return parsed.data;
|
|
9862
|
+
}
|
|
9863
|
+
function patternRequest(source, name) {
|
|
9864
|
+
const body = {
|
|
9865
|
+
name,
|
|
9866
|
+
type: source.type,
|
|
9867
|
+
detection_config: source.detection_config,
|
|
9868
|
+
...source.matching_rules != null ? { matching_rules: source.matching_rules } : {},
|
|
9869
|
+
...source.description != null ? { description: source.description } : {},
|
|
9870
|
+
...source.tags != null ? { tags: source.tags } : {}
|
|
9871
|
+
};
|
|
9872
|
+
const parsed = DataPatternRequestSchema.safeParse(structuredClone(body));
|
|
9873
|
+
if (!parsed.success)
|
|
9874
|
+
throw new Error(`Data pattern cannot be rebuilt from the backup: ${source.name}`);
|
|
9875
|
+
return parsed.data;
|
|
9876
|
+
}
|
|
9877
|
+
function portablePattern(record) {
|
|
9878
|
+
return PORTABLE_TECHNIQUES.has(record.detection_config?.technique ?? "") && record.detection_config?.technique !== void 0;
|
|
9879
|
+
}
|
|
9880
|
+
function matchByName(records, name, retired, label) {
|
|
9881
|
+
const matches = records.filter((record) => record.name === name && !retired(record));
|
|
9882
|
+
if (matches.length > 1) throw new Error(`Ambiguous destination ${label} name: ${name}`);
|
|
9883
|
+
return matches[0];
|
|
9884
|
+
}
|
|
9885
|
+
async function planDlpResourcesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9886
|
+
const backup = DlpResourcesBackupSchema.parse(input2);
|
|
9887
|
+
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9888
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
9889
|
+
pagesLimit(maxPages3);
|
|
9890
|
+
const prefix = options.namePrefix ?? "";
|
|
9891
|
+
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9892
|
+
throw new Error("Invalid --name-prefix");
|
|
9893
|
+
const conflict = options.onConflict ?? "error";
|
|
9894
|
+
if (!["error", "verify", "skip"].includes(conflict)) throw new Error("Invalid conflict policy");
|
|
9895
|
+
const patternMap2 = options.patternMap ?? {};
|
|
9896
|
+
uniqueNames(backup.dictionaries, "dictionary");
|
|
9897
|
+
uniqueNames(backup.patterns, "data pattern");
|
|
9898
|
+
uniqueNames(backup.profiles, "data profile");
|
|
9899
|
+
const unknownMapped = Object.keys(patternMap2).filter(
|
|
9900
|
+
(name) => !backup.patterns.some((p) => p.name === name)
|
|
9901
|
+
);
|
|
9902
|
+
if (unknownMapped.length)
|
|
9903
|
+
throw new Error(`--pattern-map names absent from the backup: ${unknownMapped.join(", ")}`);
|
|
9904
|
+
const crossTenant = backup.source.tsgId !== destinationTsgId;
|
|
9905
|
+
const destination = await inventories(api, maxPages3);
|
|
9906
|
+
const sourceTenant = tenantIdentity([...backup.patterns, ...backup.profiles]);
|
|
9907
|
+
const destinationTenant = tenantIdentity([...destination.patterns, ...destination.profiles]);
|
|
9908
|
+
if (sourceTenant !== void 0 && destinationTenant !== void 0) {
|
|
9909
|
+
if (crossTenant && sourceTenant === destinationTenant)
|
|
9910
|
+
throw new Error(
|
|
9911
|
+
"Destination credentials report the source DLP tenant; check the selected tenant before restoring"
|
|
9912
|
+
);
|
|
9913
|
+
if (!crossTenant && sourceTenant !== destinationTenant)
|
|
9914
|
+
throw new Error(
|
|
9915
|
+
"Destination DLP tenant does not match the backup source; use distinct tenant credentials deliberately"
|
|
9916
|
+
);
|
|
9917
|
+
}
|
|
9918
|
+
const dictionaryPlans = [];
|
|
9919
|
+
for (const source of backup.dictionaries) {
|
|
9920
|
+
if (!source.id || !source.name) throw new Error("Backup dictionary lacks identity");
|
|
9921
|
+
if (source.type === "predefined") {
|
|
9922
|
+
const match2 = matchByName(destination.dictionaries, source.name, () => false, "dictionary");
|
|
9923
|
+
if (!match2?.id || match2.type !== "predefined")
|
|
9924
|
+
throw new Error(`Missing predefined destination dictionary: ${source.name}`);
|
|
9925
|
+
dictionaryPlans.push({
|
|
9926
|
+
sourceId: source.id,
|
|
9927
|
+
name: source.name,
|
|
9928
|
+
action: "resolve",
|
|
9929
|
+
existing: structuredClone(match2)
|
|
9930
|
+
});
|
|
9931
|
+
continue;
|
|
9932
|
+
}
|
|
9933
|
+
const name = `${prefix}${source.name}`;
|
|
9934
|
+
assertName(name);
|
|
9935
|
+
const metadata = dictionaryRequest(source, name);
|
|
9936
|
+
const keywords = keywordSet(source.keywords);
|
|
9937
|
+
if (!keywords) throw new Error(`Backup dictionary lacks keywords: ${source.name}`);
|
|
9938
|
+
const match = matchByName(destination.dictionaries, name, () => false, "dictionary");
|
|
9939
|
+
if (!match) {
|
|
9940
|
+
dictionaryPlans.push({ sourceId: source.id, name, action: "create", metadata, keywords });
|
|
9941
|
+
continue;
|
|
9942
|
+
}
|
|
9943
|
+
if (!match.id) throw new Error(`Destination dictionary has no identity: ${name}`);
|
|
9944
|
+
if (!dictionaryEcho(metadata, match, keywords).matches)
|
|
9945
|
+
throw new Error(
|
|
9946
|
+
`Destination dictionary differs: ${name}; use --name-prefix to avoid changing a shared dictionary`
|
|
9947
|
+
);
|
|
9948
|
+
dictionaryPlans.push({
|
|
9949
|
+
sourceId: source.id,
|
|
9950
|
+
name,
|
|
9951
|
+
action: "reuse",
|
|
9952
|
+
metadata,
|
|
9953
|
+
keywords,
|
|
9954
|
+
existing: structuredClone(match)
|
|
9955
|
+
});
|
|
9956
|
+
}
|
|
9957
|
+
const patternPlans = [];
|
|
9958
|
+
for (const source of backup.patterns) {
|
|
9959
|
+
if (!source.id || !source.name) throw new Error("Backup data pattern lacks identity");
|
|
9960
|
+
const retired = (record) => RETIRED_PATTERN.has(record.status ?? "active");
|
|
9961
|
+
if (source.type === "predefined") {
|
|
9962
|
+
const match2 = matchByName(destination.patterns, source.name, retired, "data pattern");
|
|
9963
|
+
if (!match2?.id || match2.type !== "predefined")
|
|
9964
|
+
throw new Error(`Missing predefined destination data pattern: ${source.name}`);
|
|
9965
|
+
patternPlans.push({
|
|
9966
|
+
sourceId: source.id,
|
|
9967
|
+
name: source.name,
|
|
9968
|
+
action: "resolve",
|
|
9969
|
+
existing: structuredClone(match2)
|
|
9970
|
+
});
|
|
9971
|
+
continue;
|
|
9972
|
+
}
|
|
9973
|
+
if (Object.hasOwn(patternMap2, source.name)) {
|
|
9974
|
+
const target = patternMap2[source.name];
|
|
9975
|
+
const match2 = matchByName(destination.patterns, target, retired, "data pattern");
|
|
9976
|
+
if (!match2?.id) throw new Error(`Missing mapped destination data pattern: ${target}`);
|
|
9977
|
+
patternPlans.push({
|
|
9978
|
+
sourceId: source.id,
|
|
9979
|
+
name: target,
|
|
9980
|
+
action: "map",
|
|
9981
|
+
existing: structuredClone(match2)
|
|
9982
|
+
});
|
|
9983
|
+
continue;
|
|
9984
|
+
}
|
|
9985
|
+
if (!portablePattern(source))
|
|
9986
|
+
throw new Error(
|
|
9987
|
+
`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>"`
|
|
9988
|
+
);
|
|
9989
|
+
const name = `${prefix}${source.name}`;
|
|
9990
|
+
assertName(name);
|
|
9991
|
+
const body = patternRequest(source, name);
|
|
9992
|
+
if (crossTenant) assertKnownReferences(body, /* @__PURE__ */ new Set(), `pattern ${source.name}`);
|
|
9993
|
+
const match = matchByName(destination.patterns, name, retired, "data pattern");
|
|
9994
|
+
if (!match) {
|
|
9995
|
+
patternPlans.push({ sourceId: source.id, name, action: "create", body });
|
|
9996
|
+
continue;
|
|
9997
|
+
}
|
|
9998
|
+
if (!match.id) throw new Error(`Destination data pattern has no identity: ${name}`);
|
|
9999
|
+
if (!compareEcho(body, match).matches)
|
|
10000
|
+
throw new Error(
|
|
10001
|
+
`Destination data pattern differs: ${name}; use --name-prefix to avoid changing a shared pattern`
|
|
10002
|
+
);
|
|
10003
|
+
patternPlans.push({
|
|
10004
|
+
sourceId: source.id,
|
|
10005
|
+
name,
|
|
10006
|
+
action: "reuse",
|
|
10007
|
+
body,
|
|
10008
|
+
existing: structuredClone(match)
|
|
10009
|
+
});
|
|
10010
|
+
}
|
|
10011
|
+
const profilePlans = [];
|
|
10012
|
+
for (const source of backup.profiles) {
|
|
10013
|
+
if (!source.name) throw new Error("Backup data profile lacks identity");
|
|
10014
|
+
if (source.type === "predefined") throw new Error("Backup contains a predefined data profile");
|
|
10015
|
+
const name = `${prefix}${source.name}`;
|
|
10016
|
+
assertName(name);
|
|
10017
|
+
const match = matchByName(
|
|
10018
|
+
destination.profiles,
|
|
10019
|
+
name,
|
|
10020
|
+
(record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
|
|
10021
|
+
"data profile"
|
|
10022
|
+
);
|
|
10023
|
+
if (match && conflict === "error")
|
|
10024
|
+
throw new Error(
|
|
10025
|
+
`Destination data profile already exists: ${name}; choose --on-conflict verify or skip`
|
|
10026
|
+
);
|
|
10027
|
+
if (match && !match.id) throw new Error(`Destination data profile has no identity: ${name}`);
|
|
10028
|
+
profilePlans.push({
|
|
10029
|
+
name,
|
|
10030
|
+
source: structuredClone(source),
|
|
10031
|
+
existing: match ? structuredClone(match) : void 0,
|
|
10032
|
+
action: match ? conflict === "skip" ? "skip" : "verify" : "create"
|
|
10033
|
+
});
|
|
10034
|
+
}
|
|
10035
|
+
const plan = {
|
|
10036
|
+
sourceTsgId: backup.source.tsgId,
|
|
10037
|
+
destinationTsgId,
|
|
10038
|
+
dictionaries: dictionaryPlans,
|
|
10039
|
+
patterns: patternPlans,
|
|
10040
|
+
profiles: profilePlans,
|
|
10041
|
+
maxPages: maxPages3
|
|
10042
|
+
};
|
|
10043
|
+
const bindings = planBindings(plan);
|
|
10044
|
+
const planned = /* @__PURE__ */ new Set([
|
|
10045
|
+
...dictionaryPlans.map((d) => d.sourceId),
|
|
10046
|
+
...patternPlans.map((p) => p.sourceId)
|
|
10047
|
+
]);
|
|
10048
|
+
for (const item of profilePlans) {
|
|
10049
|
+
if (item.action === "skip") continue;
|
|
10050
|
+
for (const leaf of ruleLeaves(item.source)) {
|
|
10051
|
+
const id = leaf.value.id;
|
|
10052
|
+
if (id != null && id !== "" && !planned.has(String(id)))
|
|
10053
|
+
throw new Error(
|
|
10054
|
+
`Backup data profile references a resource absent from the backup: ${item.name}`
|
|
10055
|
+
);
|
|
10056
|
+
}
|
|
10057
|
+
const body = profileRequest(item, bindings, crossTenant, item.action === "verify");
|
|
10058
|
+
if (item.action === "verify" && body && !compareEcho(body, item.existing).matches)
|
|
10059
|
+
throw new Error(
|
|
10060
|
+
`Destination data profile does not match restore plan: ${item.name}; verification made no changes`
|
|
10061
|
+
);
|
|
10062
|
+
}
|
|
10063
|
+
return plan;
|
|
10064
|
+
}
|
|
10065
|
+
function planBindings(plan) {
|
|
10066
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
10067
|
+
for (const item of plan.dictionaries)
|
|
10068
|
+
if (item.existing?.id) bindings.set(item.sourceId, { id: item.existing.id, name: item.name });
|
|
10069
|
+
for (const item of plan.patterns)
|
|
10070
|
+
if (item.existing?.id)
|
|
10071
|
+
bindings.set(item.sourceId, {
|
|
10072
|
+
id: item.existing.id,
|
|
10073
|
+
name: item.name,
|
|
10074
|
+
version: item.existing.version ?? void 0
|
|
10075
|
+
});
|
|
10076
|
+
return bindings;
|
|
10077
|
+
}
|
|
10078
|
+
function profileRequest(item, bindings, crossTenant, requireBindings) {
|
|
10079
|
+
const detectionRules = structuredClone(item.source.detection_rules ?? []);
|
|
10080
|
+
const known = /* @__PURE__ */ new Set();
|
|
10081
|
+
let deferred = false;
|
|
10082
|
+
const remapped = { ...structuredClone(item.source), detection_rules: detectionRules };
|
|
10083
|
+
for (const leaf of ruleLeaves(remapped)) {
|
|
10084
|
+
known.add(leaf.value);
|
|
10085
|
+
const id = leaf.value.id;
|
|
10086
|
+
if (id == null || id === "") continue;
|
|
10087
|
+
const binding = bindings.get(String(id));
|
|
10088
|
+
if (!binding) {
|
|
10089
|
+
if (requireBindings)
|
|
10090
|
+
throw new Error(
|
|
10091
|
+
`Missing planned dependency binding; an existing data profile cannot be verified without its destination dependencies: ${item.name}`
|
|
10092
|
+
);
|
|
10093
|
+
deferred = true;
|
|
10094
|
+
continue;
|
|
10095
|
+
}
|
|
10096
|
+
leaf.value.id = binding.id;
|
|
10097
|
+
if (leaf.value.name != null) leaf.value.name = binding.name;
|
|
10098
|
+
if (leaf.value.version != null) {
|
|
10099
|
+
if (binding.version === void 0)
|
|
10100
|
+
throw new Error(`Destination data pattern version is unknown: ${binding.name}`);
|
|
10101
|
+
leaf.value.version = binding.version;
|
|
10102
|
+
}
|
|
10103
|
+
}
|
|
10104
|
+
const body = {
|
|
10105
|
+
name: item.name,
|
|
10106
|
+
detection_rules: detectionRules,
|
|
10107
|
+
...item.source.profile_type != null ? { profile_type: item.source.profile_type } : {},
|
|
10108
|
+
...item.source.description != null ? { description: item.source.description } : {},
|
|
10109
|
+
...item.source.is_granular_data_profile != null ? { is_granular_data_profile: item.source.is_granular_data_profile } : {}
|
|
10110
|
+
};
|
|
10111
|
+
if (crossTenant) assertKnownReferences(body, known, `profile ${item.name}`);
|
|
10112
|
+
const parsed = AdvancedDataProfileRequestSchema.parse(body);
|
|
10113
|
+
return deferred ? void 0 : parsed;
|
|
10114
|
+
}
|
|
10115
|
+
var DEPENDENCY_ACTIONS = {
|
|
10116
|
+
create: "created",
|
|
10117
|
+
reuse: "reused",
|
|
10118
|
+
resolve: "resolved",
|
|
10119
|
+
map: "mapped"
|
|
10120
|
+
};
|
|
10121
|
+
function assertUnchanged(current, prior, label) {
|
|
10122
|
+
if (canonical(current ?? null) !== canonical(prior ?? null))
|
|
10123
|
+
throw new Error(`Destination ${label} changed after planning`);
|
|
10124
|
+
}
|
|
10125
|
+
async function restoreDlpResources(api, plan) {
|
|
10126
|
+
const result = {
|
|
10127
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10128
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10129
|
+
complete: false,
|
|
10130
|
+
dictionaries: [],
|
|
10131
|
+
patterns: [],
|
|
10132
|
+
profiles: [],
|
|
10133
|
+
serverAdded: []
|
|
10134
|
+
};
|
|
10135
|
+
const crossTenant = plan.sourceTsgId !== plan.destinationTsgId;
|
|
10136
|
+
try {
|
|
10137
|
+
const current = await inventories(api, plan.maxPages);
|
|
10138
|
+
for (const item of plan.dictionaries) {
|
|
10139
|
+
const match = matchByName(current.dictionaries, item.name, () => false, "dictionary");
|
|
10140
|
+
if (item.existing) assertUnchanged(match, item.existing, `dictionary: ${item.name}`);
|
|
10141
|
+
else if (match)
|
|
10142
|
+
throw new Error(`Destination dictionary changed after planning: ${item.name}`);
|
|
10143
|
+
}
|
|
10144
|
+
const retiredPattern = (record) => RETIRED_PATTERN.has(record.status ?? "active");
|
|
10145
|
+
for (const item of plan.patterns) {
|
|
10146
|
+
const match = matchByName(current.patterns, item.name, retiredPattern, "data pattern");
|
|
10147
|
+
if (item.existing) assertUnchanged(match, item.existing, `data pattern: ${item.name}`);
|
|
10148
|
+
else if (match)
|
|
10149
|
+
throw new Error(`Destination data pattern changed after planning: ${item.name}`);
|
|
10150
|
+
}
|
|
10151
|
+
for (const item of plan.profiles) {
|
|
10152
|
+
const match = matchByName(
|
|
10153
|
+
current.profiles,
|
|
10154
|
+
item.name,
|
|
10155
|
+
(record) => RETIRED_PROFILE.has(record.profile_status ?? "active"),
|
|
10156
|
+
"data profile"
|
|
10157
|
+
);
|
|
10158
|
+
if (item.existing) assertUnchanged(match, item.existing, `data profile: ${item.name}`);
|
|
10159
|
+
else if (match)
|
|
10160
|
+
throw new Error(`Destination data profile changed after planning: ${item.name}`);
|
|
10161
|
+
}
|
|
10162
|
+
const bindings = planBindings(plan);
|
|
10163
|
+
for (const item of plan.dictionaries) {
|
|
10164
|
+
if (item.action !== "create") {
|
|
10165
|
+
const id = item.existing?.id;
|
|
10166
|
+
if (!id) throw new Error(`Destination dictionary has no identity: ${item.name}`);
|
|
10167
|
+
result.dictionaries.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
|
|
10168
|
+
continue;
|
|
10169
|
+
}
|
|
10170
|
+
if (!item.metadata || !item.keywords) throw new Error("Dictionary plan is incomplete");
|
|
10171
|
+
const created = await api.dictionaries.create({
|
|
10172
|
+
metadata: item.metadata,
|
|
10173
|
+
file: `${item.keywords.join("\n")}
|
|
10174
|
+
`,
|
|
10175
|
+
includeKeywords: true
|
|
10176
|
+
});
|
|
10177
|
+
if (!created.id) throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10178
|
+
const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
|
|
10179
|
+
const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
|
|
10180
|
+
if (!echo.matches || readBack.name !== item.name)
|
|
10181
|
+
throw new Error(`Restored dictionary did not verify: ${item.name}`);
|
|
10182
|
+
if (echo.serverAdded.length)
|
|
10183
|
+
result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
|
|
10184
|
+
bindings.set(item.sourceId, { id: created.id, name: item.name });
|
|
10185
|
+
result.dictionaries.push({ name: item.name, action: "created", id: created.id });
|
|
10186
|
+
}
|
|
10187
|
+
for (const item of plan.patterns) {
|
|
10188
|
+
if (item.action !== "create") {
|
|
10189
|
+
const id = item.existing?.id;
|
|
10190
|
+
if (!id) throw new Error(`Destination data pattern has no identity: ${item.name}`);
|
|
10191
|
+
result.patterns.push({ name: item.name, action: DEPENDENCY_ACTIONS[item.action], id });
|
|
10192
|
+
continue;
|
|
10193
|
+
}
|
|
10194
|
+
if (!item.body) throw new Error("Data pattern plan is incomplete");
|
|
10195
|
+
const created = await api.patterns.create(item.body);
|
|
10196
|
+
if (!created.id) throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10197
|
+
const readBack = await api.patterns.get(created.id);
|
|
10198
|
+
const echo = compareEcho(item.body, readBack);
|
|
10199
|
+
if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
|
|
10200
|
+
throw new Error(`Restored data pattern did not verify: ${item.name}`);
|
|
10201
|
+
if (echo.serverAdded.length)
|
|
10202
|
+
result.serverAdded.push({
|
|
10203
|
+
resource: `data pattern ${item.name}`,
|
|
10204
|
+
fields: echo.serverAdded
|
|
10205
|
+
});
|
|
10206
|
+
bindings.set(item.sourceId, {
|
|
10207
|
+
id: created.id,
|
|
10208
|
+
name: item.name,
|
|
10209
|
+
version: readBack.version ?? void 0
|
|
10210
|
+
});
|
|
10211
|
+
result.patterns.push({ name: item.name, action: "created", id: created.id });
|
|
10212
|
+
}
|
|
10213
|
+
for (const item of plan.profiles) {
|
|
10214
|
+
if (item.action === "skip") {
|
|
10215
|
+
result.profiles.push({
|
|
10216
|
+
name: item.name,
|
|
10217
|
+
action: "skipped",
|
|
10218
|
+
id: item.existing?.id ?? void 0
|
|
10219
|
+
});
|
|
10220
|
+
continue;
|
|
10221
|
+
}
|
|
10222
|
+
const body = profileRequest(item, bindings, crossTenant, true);
|
|
10223
|
+
if (!body) throw new Error(`Missing planned dependency binding: ${item.name}`);
|
|
10224
|
+
if (item.action === "verify") {
|
|
10225
|
+
const echo2 = compareEcho(body, item.existing);
|
|
10226
|
+
if (!echo2.matches)
|
|
10227
|
+
throw new Error(`Destination data profile does not match restore plan: ${item.name}`);
|
|
10228
|
+
if (echo2.serverAdded.length)
|
|
10229
|
+
result.serverAdded.push({
|
|
10230
|
+
resource: `data profile ${item.name}`,
|
|
10231
|
+
fields: echo2.serverAdded
|
|
10232
|
+
});
|
|
10233
|
+
result.profiles.push({
|
|
10234
|
+
name: item.name,
|
|
10235
|
+
action: "verified",
|
|
10236
|
+
id: item.existing?.id ?? void 0
|
|
10237
|
+
});
|
|
10238
|
+
continue;
|
|
10239
|
+
}
|
|
10240
|
+
const created = await api.profiles.create(body);
|
|
10241
|
+
if (!created.id) throw new Error(`Restored data profile did not verify: ${item.name}`);
|
|
10242
|
+
const readBack = await api.profiles.get(created.id);
|
|
10243
|
+
const echo = compareEcho(body, readBack);
|
|
10244
|
+
if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
|
|
10245
|
+
throw new Error(`Restored data profile did not verify: ${item.name}`);
|
|
10246
|
+
if (echo.serverAdded.length)
|
|
10247
|
+
result.serverAdded.push({
|
|
10248
|
+
resource: `data profile ${item.name}`,
|
|
10249
|
+
fields: echo.serverAdded
|
|
10250
|
+
});
|
|
10251
|
+
result.profiles.push({ name: item.name, action: "created", id: created.id });
|
|
10252
|
+
}
|
|
10253
|
+
result.complete = true;
|
|
10254
|
+
} catch (error) {
|
|
10255
|
+
const status = error?.statusCode;
|
|
10256
|
+
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(
|
|
10257
|
+
error.message
|
|
10258
|
+
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
10259
|
+
}
|
|
10260
|
+
return result;
|
|
10261
|
+
}
|
|
10262
|
+
|
|
10263
|
+
// src/cli/commands/dlp/transfer.ts
|
|
10264
|
+
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
10265
|
+
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
10266
|
+
function maxPages(value) {
|
|
10267
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
10268
|
+
usageError("--max-pages must be an integer from 1 to 1000");
|
|
10269
|
+
return Number(value);
|
|
10270
|
+
}
|
|
10271
|
+
function resourceKinds(value) {
|
|
10272
|
+
const kinds = value.split(",").map((kind) => kind.trim());
|
|
10273
|
+
if (!kinds.length || kinds.some((kind) => !DLP_RESOURCE_KINDS.includes(kind)) || new Set(kinds).size !== kinds.length)
|
|
10274
|
+
throw new CliUsageError("--resources takes a unique list of dictionaries, patterns, profiles");
|
|
10275
|
+
return kinds;
|
|
10276
|
+
}
|
|
10277
|
+
function patternMap(values) {
|
|
10278
|
+
const entries = values.map((value) => {
|
|
10279
|
+
const at = value.indexOf("=");
|
|
10280
|
+
if (at < 1 || at === value.length - 1)
|
|
10281
|
+
throw new CliUsageError("--pattern-map requires source-name=destination-name");
|
|
10282
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
10283
|
+
});
|
|
10284
|
+
if (new Set(entries.map(([key]) => key)).size !== entries.length)
|
|
10285
|
+
throw new CliUsageError("Duplicate source names in --pattern-map");
|
|
10286
|
+
return Object.fromEntries(entries);
|
|
10287
|
+
}
|
|
10288
|
+
async function context() {
|
|
10289
|
+
const config = await loadConfig();
|
|
10290
|
+
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
10291
|
+
throw new CliUsageError(
|
|
10292
|
+
"DLP transfer requires configured Management client ID, secret and TSG ID"
|
|
10293
|
+
);
|
|
10294
|
+
const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
|
|
10295
|
+
return {
|
|
10296
|
+
api: {
|
|
10297
|
+
dictionaries: client.dlp.dictionaries,
|
|
10298
|
+
patterns: client.dlp.dataPatterns,
|
|
10299
|
+
profiles: client.dlp.dataProfiles
|
|
10300
|
+
},
|
|
10301
|
+
tsgId: config.mgmtTsgId
|
|
10302
|
+
};
|
|
10303
|
+
}
|
|
10304
|
+
function failure(error) {
|
|
10305
|
+
if (error instanceof CliUsageError) usageError(error.message);
|
|
10306
|
+
if (error instanceof ZodError) {
|
|
10307
|
+
ui.error("Invalid DLP backup/request schema; content is not logged.");
|
|
10308
|
+
} else if (error instanceof AISecSDKException2) {
|
|
10309
|
+
ui.error(
|
|
10310
|
+
`DLP transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10311
|
+
);
|
|
10312
|
+
} else if (error?.code) {
|
|
10313
|
+
ui.error(
|
|
10314
|
+
`DLP transfer file operation failed (${error.code}); existing files are not overwritten.`
|
|
10315
|
+
);
|
|
10316
|
+
} else {
|
|
10317
|
+
ui.error(error instanceof Error ? error.message : "DLP transfer failed");
|
|
10318
|
+
}
|
|
10319
|
+
process.exitCode = 1;
|
|
10320
|
+
}
|
|
10321
|
+
function renderRestore(value, columns, format) {
|
|
10322
|
+
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10323
|
+
return formatOutput([value], columns, format);
|
|
10324
|
+
const markdown = format === "markdown";
|
|
10325
|
+
const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
|
|
10326
|
+
const heading = (text2) => markdown ? `### ${text2}
|
|
10327
|
+
` : text2;
|
|
10328
|
+
const blocks = [
|
|
10329
|
+
heading(
|
|
10330
|
+
value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
|
|
10331
|
+
),
|
|
10332
|
+
`Source TSG: ${value.sourceTsgId}
|
|
10333
|
+
${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
10334
|
+
];
|
|
10335
|
+
for (const [key, title] of [
|
|
10336
|
+
["dictionaries", "Dictionaries"],
|
|
10337
|
+
["patterns", "Data patterns"],
|
|
10338
|
+
["profiles", "Data profiles"]
|
|
10339
|
+
]) {
|
|
10340
|
+
blocks.push(heading(`${title} (${rows(key).length})`));
|
|
10341
|
+
blocks.push(
|
|
10342
|
+
formatOutput(
|
|
10343
|
+
rows(key),
|
|
10344
|
+
[
|
|
10345
|
+
{ key: "action", label: "Action" },
|
|
10346
|
+
{ key: "name", label: "Name" }
|
|
10347
|
+
],
|
|
10348
|
+
markdown ? "markdown" : "table"
|
|
10349
|
+
) || "(none)"
|
|
10350
|
+
);
|
|
10351
|
+
}
|
|
10352
|
+
if (rows("serverAdded").length) {
|
|
10353
|
+
blocks.push(heading("Verified server-added fields (source omitted these)"));
|
|
10354
|
+
for (const item of rows("serverAdded"))
|
|
10355
|
+
blocks.push(
|
|
10356
|
+
`${item.resource}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
|
|
10357
|
+
);
|
|
10358
|
+
}
|
|
10359
|
+
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10360
|
+
return blocks.join("\n\n");
|
|
10361
|
+
}
|
|
10362
|
+
function register6(dlp) {
|
|
10363
|
+
const backup = dlp.command("backup").description(
|
|
10364
|
+
"Back up custom DLP dictionaries (with keywords), data patterns and data profiles with their referenced dependencies"
|
|
10365
|
+
).option(
|
|
10366
|
+
"--resources <list>",
|
|
10367
|
+
"Comma-separated primary resources: dictionaries, patterns, profiles (default: all)",
|
|
10368
|
+
resourceKinds
|
|
10369
|
+
).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(
|
|
10370
|
+
"--skip-unsupported",
|
|
10371
|
+
"Exclude profiles with non-transferable rules (multi-profile, direct EDM) instead of failing"
|
|
10372
|
+
).option(
|
|
10373
|
+
"--max-pages <n>",
|
|
10374
|
+
"Maximum pages per inventory; incomplete exports fail",
|
|
10375
|
+
maxPages,
|
|
10376
|
+
100
|
|
10377
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10378
|
+
"after",
|
|
10379
|
+
examples(
|
|
10380
|
+
"airs runtime dlp backup --output-file ./dlp-backup.json",
|
|
10381
|
+
"airs runtime dlp backup --resources profiles --skip-unsupported --output-file ./profiles.yaml --file-format yaml"
|
|
10382
|
+
)
|
|
10383
|
+
).action(async (opts) => {
|
|
10384
|
+
try {
|
|
10385
|
+
if (!["json", "yaml"].includes(opts.fileFormat))
|
|
10386
|
+
throw new CliUsageError("--file-format must be json or yaml");
|
|
10387
|
+
const format = await resolveOutput(backup, opts);
|
|
10388
|
+
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10389
|
+
if (opts.outputFile === "-")
|
|
10390
|
+
throw new CliUsageError(
|
|
10391
|
+
"Backups contain dictionary keywords and require a private file destination, not stdout"
|
|
10392
|
+
);
|
|
10393
|
+
const path3 = resolve5(
|
|
10394
|
+
opts.outputFile ?? `airs-dlp-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
|
|
10395
|
+
);
|
|
10396
|
+
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10397
|
+
extname(path3).toLowerCase()
|
|
10398
|
+
))
|
|
10399
|
+
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10400
|
+
const { api, tsgId } = await context();
|
|
10401
|
+
const { backup: data, skipped } = await backupDlpResources(api, tsgId, {
|
|
10402
|
+
resources: opts.resources,
|
|
10403
|
+
maxPages: opts.maxPages,
|
|
10404
|
+
skipUnsupported: Boolean(opts.skipUnsupported)
|
|
10405
|
+
});
|
|
10406
|
+
for (const item of skipped)
|
|
10407
|
+
ui.warning(`Skipped unsupported data profile: ${item.profile} (${item.reason})`);
|
|
10408
|
+
const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10409
|
+
`;
|
|
10410
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
|
|
10411
|
+
throw new Error("Backup exceeds the 20 MiB size limit; select fewer --resources");
|
|
10412
|
+
await writeReportFile(path3, content);
|
|
10413
|
+
console.log(
|
|
10414
|
+
formatOutput(
|
|
10415
|
+
[
|
|
10416
|
+
{
|
|
10417
|
+
file: path3,
|
|
10418
|
+
sourceTsgId: tsgId,
|
|
10419
|
+
dictionaries: data.dictionaries.length,
|
|
10420
|
+
patterns: data.patterns.length,
|
|
10421
|
+
profiles: data.profiles.length,
|
|
10422
|
+
skipped: skipped.length
|
|
10423
|
+
}
|
|
10424
|
+
],
|
|
10425
|
+
[
|
|
10426
|
+
{ key: "file", label: "Backup file" },
|
|
10427
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10428
|
+
{ key: "dictionaries", label: "Dictionaries" },
|
|
10429
|
+
{ key: "patterns", label: "Patterns" },
|
|
10430
|
+
{ key: "profiles", label: "Profiles" },
|
|
10431
|
+
{ key: "skipped", label: "Skipped" }
|
|
10432
|
+
],
|
|
10433
|
+
format === "pretty" ? "table" : format
|
|
10434
|
+
)
|
|
10435
|
+
);
|
|
10436
|
+
} catch (error) {
|
|
10437
|
+
failure(error);
|
|
10438
|
+
}
|
|
10439
|
+
});
|
|
10440
|
+
const restore = dlp.command("restore <file>").description(
|
|
10441
|
+
"Restore DLP resources into the selected tenant, staged dictionaries \u2192 patterns \u2192 profiles; validate all dependencies before changes"
|
|
10442
|
+
).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(
|
|
10443
|
+
"--on-conflict <policy>",
|
|
10444
|
+
"Existing data profile names: error, verify (resume without writes) or skip. Updates are unsupported until the DLP profile write path is live-verified.",
|
|
10445
|
+
"error"
|
|
10446
|
+
).option(
|
|
10447
|
+
"--pattern-map <source=destination>",
|
|
10448
|
+
"Bind a tenant-bound source pattern to an existing destination pattern (repeatable)",
|
|
10449
|
+
(value, prior) => [...prior, value],
|
|
10450
|
+
[]
|
|
10451
|
+
).option(
|
|
10452
|
+
"--max-pages <n>",
|
|
10453
|
+
"Maximum pages per inventory; incomplete inventories fail",
|
|
10454
|
+
maxPages,
|
|
10455
|
+
100
|
|
10456
|
+
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10457
|
+
"--force",
|
|
10458
|
+
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10459
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10460
|
+
"after",
|
|
10461
|
+
examples(
|
|
10462
|
+
"airs runtime dlp restore ./dlp-backup.json --dry-run --output json",
|
|
10463
|
+
'airs runtime dlp restore ./dlp-backup.json --pattern-map "EDM SSN=Dest SSN" --dry-run',
|
|
10464
|
+
"airs runtime dlp restore ./dlp-backup.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10465
|
+
)
|
|
10466
|
+
).action(async (file, opts) => {
|
|
10467
|
+
try {
|
|
10468
|
+
if (!["error", "verify", "skip"].includes(opts.onConflict))
|
|
10469
|
+
throw new CliUsageError("--on-conflict must be error, verify or skip");
|
|
10470
|
+
if (opts.force && !opts.expectTsg)
|
|
10471
|
+
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10472
|
+
const mappings = patternMap(opts.patternMap);
|
|
10473
|
+
const path3 = resolve5(file);
|
|
10474
|
+
const extension = extname(path3).toLowerCase();
|
|
10475
|
+
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10476
|
+
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10477
|
+
const info = await stat(path3);
|
|
10478
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10479
|
+
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10480
|
+
let input2;
|
|
10481
|
+
try {
|
|
10482
|
+
const text2 = await readFile9(path3, "utf8");
|
|
10483
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10484
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10485
|
+
} catch {
|
|
10486
|
+
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10487
|
+
}
|
|
10488
|
+
const data = DlpResourcesBackupSchema.parse(input2);
|
|
10489
|
+
const format = await resolveOutput(restore, opts);
|
|
10490
|
+
const { api, tsgId } = await context();
|
|
10491
|
+
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10492
|
+
throw new CliUsageError(
|
|
10493
|
+
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
10494
|
+
);
|
|
10495
|
+
const plan = await planDlpResourcesRestore(api, data, tsgId, {
|
|
10496
|
+
namePrefix: opts.namePrefix,
|
|
10497
|
+
onConflict: opts.onConflict,
|
|
10498
|
+
patternMap: mappings,
|
|
10499
|
+
maxPages: opts.maxPages
|
|
10500
|
+
});
|
|
10501
|
+
const summary = {
|
|
10502
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10503
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10504
|
+
dryRun: Boolean(opts.dryRun),
|
|
10505
|
+
dictionaries: plan.dictionaries.map((d) => ({ name: d.name, action: d.action })),
|
|
10506
|
+
patterns: plan.patterns.map((p) => ({ name: p.name, action: p.action })),
|
|
10507
|
+
profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action }))
|
|
10508
|
+
};
|
|
10509
|
+
const columns = [
|
|
10510
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10511
|
+
{ key: "destinationTsgId", label: "Destination TSG" },
|
|
10512
|
+
{ key: "dictionaries", label: "Dictionaries" },
|
|
10513
|
+
{ key: "patterns", label: "Data patterns" },
|
|
10514
|
+
{ key: "profiles", label: "Data profiles" },
|
|
10515
|
+
{ key: "serverAdded", label: "Verified server-added fields" },
|
|
10516
|
+
{ key: "dryRun", label: "Dry run" },
|
|
10517
|
+
{ key: "complete", label: "Complete" }
|
|
10518
|
+
];
|
|
10519
|
+
if (opts.dryRun) {
|
|
10520
|
+
console.log(renderRestore(summary, columns, format));
|
|
10521
|
+
return;
|
|
10522
|
+
}
|
|
10523
|
+
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;
|
|
10524
|
+
if (writes)
|
|
10525
|
+
await confirmOrAbort(
|
|
10526
|
+
`Restore ${writes} DLP resources from TSG ${plan.sourceTsgId} to TSG ${tsgId}?`,
|
|
10527
|
+
Boolean(opts.force),
|
|
10528
|
+
{ action: `restore DLP resources into TSG ${tsgId}` }
|
|
10529
|
+
);
|
|
10530
|
+
const result = await restoreDlpResources(api, plan);
|
|
10531
|
+
console.log(renderRestore({ ...result }, columns, format));
|
|
10532
|
+
if (!result.complete) {
|
|
10533
|
+
ui.error(result.error ?? "Restore incomplete");
|
|
10534
|
+
process.exitCode = 1;
|
|
10535
|
+
}
|
|
10536
|
+
} catch (error) {
|
|
10537
|
+
failure(error);
|
|
10538
|
+
}
|
|
10539
|
+
});
|
|
10540
|
+
}
|
|
10541
|
+
|
|
9553
10542
|
// src/cli/commands/dlp/index.ts
|
|
9554
10543
|
function registerDlpCommands(runtime) {
|
|
9555
|
-
const dlp = runtime.command("dlp").description(
|
|
10544
|
+
const dlp = runtime.command("dlp").description(
|
|
10545
|
+
"DLP management (filtering-profiles, patterns, profiles, dictionaries, generate, backup/restore)"
|
|
10546
|
+
);
|
|
9556
10547
|
register2(dlp);
|
|
9557
10548
|
register4(dlp);
|
|
9558
10549
|
register5(dlp);
|
|
9559
10550
|
register(dlp);
|
|
9560
10551
|
register3(dlp);
|
|
10552
|
+
register6(dlp);
|
|
9561
10553
|
}
|
|
9562
10554
|
|
|
9563
10555
|
// 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";
|
|
10556
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10557
|
+
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
10558
|
+
import { extname as extname2, resolve as resolve6 } from "path";
|
|
10559
|
+
import { AISecSDKException as AISecSDKException3, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
10560
|
+
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
10561
|
+
import { ZodError as ZodError2 } from "zod";
|
|
9570
10562
|
|
|
9571
10563
|
// src/backup/runtime-profiles.ts
|
|
9572
10564
|
import {
|
|
@@ -9575,10 +10567,10 @@ import {
|
|
|
9575
10567
|
PolicySchema,
|
|
9576
10568
|
SecurityProfileSchema
|
|
9577
10569
|
} from "@cdot65/prisma-airs-sdk";
|
|
9578
|
-
import { z as
|
|
10570
|
+
import { z as z6 } from "zod";
|
|
9579
10571
|
|
|
9580
10572
|
// src/backup/runtime-policy.ts
|
|
9581
|
-
function
|
|
10573
|
+
function object2(value) {
|
|
9582
10574
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9583
10575
|
}
|
|
9584
10576
|
function compareRuntimePolicies(expected, actual) {
|
|
@@ -9599,7 +10591,7 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9599
10591
|
});
|
|
9600
10592
|
return;
|
|
9601
10593
|
}
|
|
9602
|
-
if (
|
|
10594
|
+
if (object2(source) && object2(target)) {
|
|
9603
10595
|
for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
|
|
9604
10596
|
const field = `${path3}.${key}`;
|
|
9605
10597
|
if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
|
|
@@ -9630,14 +10622,14 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9630
10622
|
}
|
|
9631
10623
|
if (relative === "model.app-protection" && key === "url-detected-severity")
|
|
9632
10624
|
allowed = "low";
|
|
9633
|
-
if (relative === "model.app-protection" && key === "default-url-category" &&
|
|
10625
|
+
if (relative === "model.app-protection" && key === "default-url-category" && object2(target[key])) {
|
|
9634
10626
|
const value = target[key];
|
|
9635
10627
|
if (Object.keys(value).length === 1 && value.member === null) {
|
|
9636
10628
|
serverDefaults.push(field);
|
|
9637
10629
|
continue;
|
|
9638
10630
|
}
|
|
9639
10631
|
}
|
|
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" &&
|
|
10632
|
+
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
10633
|
const value = target[key];
|
|
9642
10634
|
if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
|
|
9643
10635
|
serverDefaults.push(field);
|
|
@@ -9672,26 +10664,26 @@ function compareRuntimePolicies(expected, actual) {
|
|
|
9672
10664
|
}
|
|
9673
10665
|
|
|
9674
10666
|
// src/backup/runtime-profiles.ts
|
|
9675
|
-
var RuntimeProfilesBackupSchema =
|
|
9676
|
-
version:
|
|
9677
|
-
resourceType:
|
|
9678
|
-
exportedAt:
|
|
9679
|
-
source:
|
|
9680
|
-
profiles:
|
|
9681
|
-
topics:
|
|
10667
|
+
var RuntimeProfilesBackupSchema = z6.object({
|
|
10668
|
+
version: z6.literal(1),
|
|
10669
|
+
resourceType: z6.literal("runtime-security-profiles"),
|
|
10670
|
+
exportedAt: z6.string().datetime({ offset: true }),
|
|
10671
|
+
source: z6.object({ tsgId: z6.string().min(1) }).strict(),
|
|
10672
|
+
profiles: z6.array(SecurityProfileSchema).min(1).max(1e4),
|
|
10673
|
+
topics: z6.array(CustomTopicSchema).max(1e4)
|
|
9682
10674
|
}).strict();
|
|
9683
|
-
function
|
|
10675
|
+
function object3(value) {
|
|
9684
10676
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9685
10677
|
}
|
|
9686
|
-
function
|
|
10678
|
+
function pagesLimit2(value) {
|
|
9687
10679
|
if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
|
|
9688
10680
|
throw new Error("maxPages must be an integer from 1 to 1000");
|
|
9689
10681
|
}
|
|
9690
|
-
async function offsetPages(fetch,
|
|
9691
|
-
|
|
10682
|
+
async function offsetPages(fetch, maxPages3) {
|
|
10683
|
+
pagesLimit2(maxPages3);
|
|
9692
10684
|
const items = [];
|
|
9693
10685
|
let offset = 0;
|
|
9694
|
-
for (let page = 0; page <
|
|
10686
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
9695
10687
|
const result = await fetch(offset);
|
|
9696
10688
|
items.push(...result.items);
|
|
9697
10689
|
if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
|
|
@@ -9706,7 +10698,7 @@ function latestProfiles(profiles2) {
|
|
|
9706
10698
|
const names = /* @__PURE__ */ new Map();
|
|
9707
10699
|
const revisions = /* @__PURE__ */ new Set();
|
|
9708
10700
|
for (const profile of profiles2) {
|
|
9709
|
-
|
|
10701
|
+
assertName2(profile.profile_name);
|
|
9710
10702
|
const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
|
|
9711
10703
|
if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
|
|
9712
10704
|
revisions.add(identity2);
|
|
@@ -9718,7 +10710,7 @@ function latestProfiles(profiles2) {
|
|
|
9718
10710
|
}
|
|
9719
10711
|
return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
|
|
9720
10712
|
}
|
|
9721
|
-
function
|
|
10713
|
+
function assertName2(name) {
|
|
9722
10714
|
if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9723
10715
|
throw new Error("Profile/topic names must be nonempty and contain no control characters");
|
|
9724
10716
|
}
|
|
@@ -9726,19 +10718,19 @@ function assertTenant(profiles2, tsgId) {
|
|
|
9726
10718
|
if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
|
|
9727
10719
|
throw new Error("Destination/source profile tenant identity does not match the selected TSG");
|
|
9728
10720
|
}
|
|
9729
|
-
async function profiles(api,
|
|
10721
|
+
async function profiles(api, maxPages3) {
|
|
9730
10722
|
return latestProfiles(
|
|
9731
10723
|
await offsetPages(async (offset) => {
|
|
9732
10724
|
const page = await api.profiles.list({ offset, limit: 100, latest: true });
|
|
9733
10725
|
return { items: page.ai_profiles, next: page.next_offset };
|
|
9734
|
-
},
|
|
10726
|
+
}, maxPages3)
|
|
9735
10727
|
);
|
|
9736
10728
|
}
|
|
9737
|
-
async function topics(api,
|
|
10729
|
+
async function topics(api, maxPages3) {
|
|
9738
10730
|
return offsetPages(async (offset) => {
|
|
9739
10731
|
const page = await api.topics.list({ offset, limit: 100 });
|
|
9740
10732
|
return { items: page.custom_topics, next: page.next_offset };
|
|
9741
|
-
},
|
|
10733
|
+
}, maxPages3);
|
|
9742
10734
|
}
|
|
9743
10735
|
function topicReferences(policy) {
|
|
9744
10736
|
const refs = [];
|
|
@@ -9747,7 +10739,7 @@ function topicReferences(policy) {
|
|
|
9747
10739
|
value.forEach(visit);
|
|
9748
10740
|
return;
|
|
9749
10741
|
}
|
|
9750
|
-
if (!
|
|
10742
|
+
if (!object3(value)) return;
|
|
9751
10743
|
if ("topic_id" in value) {
|
|
9752
10744
|
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9753
10745
|
throw new Error("Invalid topic identity in profile policy");
|
|
@@ -9763,8 +10755,8 @@ function topicKey(ref) {
|
|
|
9763
10755
|
}
|
|
9764
10756
|
async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
9765
10757
|
if (!tsgId) throw new Error("Source TSG ID is required");
|
|
9766
|
-
const
|
|
9767
|
-
let selected = await profiles(api,
|
|
10758
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
10759
|
+
let selected = await profiles(api, maxPages3);
|
|
9768
10760
|
assertTenant(selected, tsgId);
|
|
9769
10761
|
if (options.profile)
|
|
9770
10762
|
selected = selected.filter(
|
|
@@ -9777,7 +10769,7 @@ async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
|
9777
10769
|
PolicySchema.parse(profile.policy);
|
|
9778
10770
|
}
|
|
9779
10771
|
const refs = selected.flatMap((profile) => topicReferences(profile.policy));
|
|
9780
|
-
const definitions = refs.length ? await topics(api,
|
|
10772
|
+
const definitions = refs.length ? await topics(api, maxPages3) : [];
|
|
9781
10773
|
const needed = /* @__PURE__ */ new Map();
|
|
9782
10774
|
for (const ref of refs) {
|
|
9783
10775
|
const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
|
|
@@ -9800,21 +10792,21 @@ function sameTopic(a, b) {
|
|
|
9800
10792
|
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9801
10793
|
}
|
|
9802
10794
|
function dlpReferences(policy) {
|
|
9803
|
-
if (!
|
|
10795
|
+
if (!object3(policy)) return [];
|
|
9804
10796
|
const refs = [];
|
|
9805
10797
|
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9806
|
-
if (!
|
|
10798
|
+
if (!object3(entry) || typeof entry.name !== "string")
|
|
9807
10799
|
throw new Error("Invalid embedded DLP reference");
|
|
9808
10800
|
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9809
10801
|
}
|
|
9810
10802
|
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9811
|
-
if (!
|
|
10803
|
+
if (!object3(entry) || !object3(entry["model-configuration"])) continue;
|
|
9812
10804
|
const protection = entry["model-configuration"]["data-protection"];
|
|
9813
|
-
if (!
|
|
10805
|
+
if (!object3(protection) || !object3(protection["data-leak-detection"])) continue;
|
|
9814
10806
|
const members = protection["data-leak-detection"].member;
|
|
9815
10807
|
if (!Array.isArray(members)) continue;
|
|
9816
10808
|
for (const member of members) {
|
|
9817
|
-
if (!
|
|
10809
|
+
if (!object3(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9818
10810
|
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
10811
|
continue;
|
|
9820
10812
|
refs.push({ value: member, name: member.text, kind: "member" });
|
|
@@ -9857,7 +10849,7 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9857
10849
|
});
|
|
9858
10850
|
return;
|
|
9859
10851
|
}
|
|
9860
|
-
if (!
|
|
10852
|
+
if (!object3(value)) return;
|
|
9861
10853
|
for (const [key, child] of Object.entries(value)) {
|
|
9862
10854
|
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9863
10855
|
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
@@ -9866,9 +10858,9 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9866
10858
|
};
|
|
9867
10859
|
visit(policy, "policy");
|
|
9868
10860
|
}
|
|
9869
|
-
async function dataProfiles(api,
|
|
10861
|
+
async function dataProfiles(api, maxPages3) {
|
|
9870
10862
|
const items = [];
|
|
9871
|
-
for (let page = 0; page <
|
|
10863
|
+
for (let page = 0; page < maxPages3; page++) {
|
|
9872
10864
|
const result = await api.dataProfiles.list({ page, size: 100 });
|
|
9873
10865
|
items.push(...result.content);
|
|
9874
10866
|
if (items.length > 1e4) throw new Error("DLP inventory exceeds the safety limit");
|
|
@@ -9881,8 +10873,8 @@ async function dataProfiles(api, maxPages2) {
|
|
|
9881
10873
|
async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9882
10874
|
const backup = RuntimeProfilesBackupSchema.parse(input2);
|
|
9883
10875
|
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9884
|
-
const
|
|
9885
|
-
|
|
10876
|
+
const maxPages3 = options.maxPages ?? 100;
|
|
10877
|
+
pagesLimit2(maxPages3);
|
|
9886
10878
|
const prefix = options.namePrefix ?? "";
|
|
9887
10879
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9888
10880
|
throw new Error("Invalid --name-prefix");
|
|
@@ -9905,7 +10897,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9905
10897
|
});
|
|
9906
10898
|
if (crossTenant) assertKnownPolicyReferences(profile.policy);
|
|
9907
10899
|
}
|
|
9908
|
-
const existing = await profiles(api,
|
|
10900
|
+
const existing = await profiles(api, maxPages3);
|
|
9909
10901
|
assertTenant(existing, destinationTsgId);
|
|
9910
10902
|
const profilePlans = selected.map((source) => {
|
|
9911
10903
|
const name = `${prefix}${source.profile_name}`;
|
|
@@ -9924,7 +10916,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9924
10916
|
});
|
|
9925
10917
|
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
9926
10918
|
const refs = changing.flatMap((p) => topicReferences(p.source.policy));
|
|
9927
|
-
const targetTopics = refs.length ? await topics(api,
|
|
10919
|
+
const targetTopics = refs.length ? await topics(api, maxPages3) : [];
|
|
9928
10920
|
const topicPlans = /* @__PURE__ */ new Map();
|
|
9929
10921
|
const topicNames = /* @__PURE__ */ new Map();
|
|
9930
10922
|
for (const ref of refs) {
|
|
@@ -9937,7 +10929,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9937
10929
|
throw new Error(`Backup lacks an unambiguous topic definition: ${ref.topic_name}`);
|
|
9938
10930
|
const source = definitions[0];
|
|
9939
10931
|
const name = `${prefix}${source.topic_name}`;
|
|
9940
|
-
|
|
10932
|
+
assertName2(name);
|
|
9941
10933
|
if (topicNames.has(name) && topicNames.get(name) !== key)
|
|
9942
10934
|
throw new Error(
|
|
9943
10935
|
`Multiple source revisions use topic name ${name}; split the backup by profile/revision before restoring`
|
|
@@ -9964,7 +10956,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9964
10956
|
);
|
|
9965
10957
|
if (!targetFor(name)) unresolved.add(name);
|
|
9966
10958
|
}
|
|
9967
|
-
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api,
|
|
10959
|
+
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages3) : [];
|
|
9968
10960
|
const resolved = [];
|
|
9969
10961
|
for (const name of names) {
|
|
9970
10962
|
const target = targetFor(name);
|
|
@@ -10015,7 +11007,7 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
10015
11007
|
dlpMappings,
|
|
10016
11008
|
dlpFallbacks,
|
|
10017
11009
|
serverDefaults: [],
|
|
10018
|
-
maxPages:
|
|
11010
|
+
maxPages: maxPages3
|
|
10019
11011
|
};
|
|
10020
11012
|
const existingTopics = new Map(
|
|
10021
11013
|
plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
|
|
@@ -10032,8 +11024,8 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
10032
11024
|
}
|
|
10033
11025
|
return plan;
|
|
10034
11026
|
}
|
|
10035
|
-
function
|
|
10036
|
-
const order = (v) => Array.isArray(v) ? v.map(order) :
|
|
11027
|
+
function canonical2(value) {
|
|
11028
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object3(v) ? Object.fromEntries(
|
|
10037
11029
|
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
10038
11030
|
) : v;
|
|
10039
11031
|
return JSON.stringify(order(value));
|
|
@@ -10044,7 +11036,7 @@ function canonicalPolicy(policy) {
|
|
|
10044
11036
|
const protection = entry["model-configuration"]?.["data-protection"];
|
|
10045
11037
|
if (protection && !("database-security" in protection)) protection["database-security"] = null;
|
|
10046
11038
|
}
|
|
10047
|
-
return
|
|
11039
|
+
return canonical2(value);
|
|
10048
11040
|
}
|
|
10049
11041
|
function unchanged(current, prior) {
|
|
10050
11042
|
return Boolean(
|
|
@@ -10187,9 +11179,9 @@ function mappedPolicy(item, plan, mappedTopics) {
|
|
|
10187
11179
|
}
|
|
10188
11180
|
|
|
10189
11181
|
// src/cli/commands/profile-transfer.ts
|
|
10190
|
-
var
|
|
10191
|
-
var
|
|
10192
|
-
function
|
|
11182
|
+
var MAX_BACKUP_BYTES2 = 20 * 1024 * 1024;
|
|
11183
|
+
var FORMATS2 = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
11184
|
+
function renderRestore2(value, columns, format) {
|
|
10193
11185
|
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10194
11186
|
return formatOutput([value], columns, format);
|
|
10195
11187
|
const markdown = format === "markdown";
|
|
@@ -10249,7 +11241,7 @@ ${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
|
10249
11241
|
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10250
11242
|
return blocks.join("\n\n");
|
|
10251
11243
|
}
|
|
10252
|
-
function
|
|
11244
|
+
function maxPages2(value) {
|
|
10253
11245
|
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
10254
11246
|
usageError("--max-pages must be an integer from 1 to 1000");
|
|
10255
11247
|
return Number(value);
|
|
@@ -10265,13 +11257,13 @@ function dlpMap(values) {
|
|
|
10265
11257
|
throw new CliUsageError("Duplicate source names in --dlp-map");
|
|
10266
11258
|
return Object.fromEntries(entries);
|
|
10267
11259
|
}
|
|
10268
|
-
async function
|
|
11260
|
+
async function context2() {
|
|
10269
11261
|
const config = await loadConfig();
|
|
10270
11262
|
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
10271
11263
|
throw new CliUsageError(
|
|
10272
11264
|
"Profile transfer requires configured Management client ID, secret and TSG ID"
|
|
10273
11265
|
);
|
|
10274
|
-
const client = new
|
|
11266
|
+
const client = new ManagementClient2({ ...managementClientOptions(config), numRetries: 0 });
|
|
10275
11267
|
return {
|
|
10276
11268
|
api: {
|
|
10277
11269
|
profiles: client.profiles,
|
|
@@ -10281,11 +11273,11 @@ async function context() {
|
|
|
10281
11273
|
tsgId: config.mgmtTsgId
|
|
10282
11274
|
};
|
|
10283
11275
|
}
|
|
10284
|
-
function
|
|
11276
|
+
function failure2(error) {
|
|
10285
11277
|
if (error instanceof CliUsageError) usageError(error.message);
|
|
10286
|
-
if (error instanceof
|
|
11278
|
+
if (error instanceof ZodError2) {
|
|
10287
11279
|
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
10288
|
-
} else if (error instanceof
|
|
11280
|
+
} else if (error instanceof AISecSDKException3) {
|
|
10289
11281
|
ui.error(
|
|
10290
11282
|
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10291
11283
|
);
|
|
@@ -10304,9 +11296,9 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10304
11296
|
).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
11297
|
"--max-pages <n>",
|
|
10306
11298
|
"Maximum pages per inventory; incomplete exports fail",
|
|
10307
|
-
|
|
11299
|
+
maxPages2,
|
|
10308
11300
|
100
|
|
10309
|
-
).option("--output <format>",
|
|
11301
|
+
).option("--output <format>", FORMATS2).addHelpText(
|
|
10310
11302
|
"after",
|
|
10311
11303
|
examples(
|
|
10312
11304
|
"airs runtime profiles backup --all --output-file ./profiles.json",
|
|
@@ -10322,18 +11314,18 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10322
11314
|
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10323
11315
|
if (opts.outputFile === "-")
|
|
10324
11316
|
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, "-")}-${
|
|
11317
|
+
const path3 = resolve6(
|
|
11318
|
+
opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID8().slice(0, 8)}${extension}`
|
|
10327
11319
|
);
|
|
10328
11320
|
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10329
|
-
|
|
11321
|
+
extname2(path3).toLowerCase()
|
|
10330
11322
|
))
|
|
10331
11323
|
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10332
|
-
const { api, tsgId } = await
|
|
11324
|
+
const { api, tsgId } = await context2();
|
|
10333
11325
|
const data = await backupRuntimeProfiles(api, tsgId, { profile, maxPages: opts.maxPages });
|
|
10334
|
-
const content = opts.fileFormat === "yaml" ?
|
|
11326
|
+
const content = opts.fileFormat === "yaml" ? dump7(data, { schema: JSON_SCHEMA2, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10335
11327
|
`;
|
|
10336
|
-
if (Buffer.byteLength(content) >
|
|
11328
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES2)
|
|
10337
11329
|
throw new Error("Backup exceeds the 20 MiB size limit; select individual profiles");
|
|
10338
11330
|
await writeReportFile(path3, content);
|
|
10339
11331
|
console.log(
|
|
@@ -10356,7 +11348,7 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10356
11348
|
)
|
|
10357
11349
|
);
|
|
10358
11350
|
} catch (error) {
|
|
10359
|
-
|
|
11351
|
+
failure2(error);
|
|
10360
11352
|
}
|
|
10361
11353
|
});
|
|
10362
11354
|
const restore = profiles2.command("restore <file>").description(
|
|
@@ -10377,12 +11369,12 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10377
11369
|
).option(
|
|
10378
11370
|
"--max-pages <n>",
|
|
10379
11371
|
"Maximum pages per inventory; incomplete inventories fail",
|
|
10380
|
-
|
|
11372
|
+
maxPages2,
|
|
10381
11373
|
100
|
|
10382
11374
|
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10383
11375
|
"--force",
|
|
10384
11376
|
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10385
|
-
).option("--output <format>",
|
|
11377
|
+
).option("--output <format>", FORMATS2).addHelpText(
|
|
10386
11378
|
"after",
|
|
10387
11379
|
examples(
|
|
10388
11380
|
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
@@ -10398,24 +11390,24 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10398
11390
|
if (opts.force && !opts.expectTsg)
|
|
10399
11391
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10400
11392
|
const mappings = dlpMap(opts.dlpMap);
|
|
10401
|
-
const path3 =
|
|
10402
|
-
const extension =
|
|
11393
|
+
const path3 = resolve6(file);
|
|
11394
|
+
const extension = extname2(path3).toLowerCase();
|
|
10403
11395
|
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10404
11396
|
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10405
|
-
const info = await
|
|
10406
|
-
if (!info.isFile() || info.size >
|
|
11397
|
+
const info = await stat2(path3);
|
|
11398
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES2)
|
|
10407
11399
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10408
11400
|
let input2;
|
|
10409
11401
|
try {
|
|
10410
|
-
const text2 = await
|
|
10411
|
-
if (Buffer.byteLength(text2) >
|
|
10412
|
-
input2 = extension === ".json" ? JSON.parse(text2) :
|
|
11402
|
+
const text2 = await readFile10(path3, "utf8");
|
|
11403
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES2) throw new Error("Backup too large");
|
|
11404
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load3(text2, { schema: JSON_SCHEMA2 });
|
|
10413
11405
|
} catch {
|
|
10414
11406
|
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10415
11407
|
}
|
|
10416
11408
|
const data = RuntimeProfilesBackupSchema.parse(input2);
|
|
10417
11409
|
const format = await resolveOutput(restore, opts);
|
|
10418
|
-
const { api, tsgId } = await
|
|
11410
|
+
const { api, tsgId } = await context2();
|
|
10419
11411
|
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10420
11412
|
throw new CliUsageError(
|
|
10421
11413
|
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
@@ -10459,7 +11451,7 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10459
11451
|
`DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
|
|
10460
11452
|
);
|
|
10461
11453
|
if (opts.dryRun) {
|
|
10462
|
-
console.log(
|
|
11454
|
+
console.log(renderRestore2(summary, columns, format));
|
|
10463
11455
|
return;
|
|
10464
11456
|
}
|
|
10465
11457
|
const changing = plan.profiles.filter(
|
|
@@ -10472,13 +11464,13 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10472
11464
|
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10473
11465
|
);
|
|
10474
11466
|
const result = await restoreRuntimeProfiles(api, plan);
|
|
10475
|
-
console.log(
|
|
11467
|
+
console.log(renderRestore2({ ...result }, columns, format));
|
|
10476
11468
|
if (!result.complete) {
|
|
10477
11469
|
ui.error(result.error ?? "Restore incomplete");
|
|
10478
11470
|
process.exitCode = 1;
|
|
10479
11471
|
}
|
|
10480
11472
|
} catch (error) {
|
|
10481
|
-
|
|
11473
|
+
failure2(error);
|
|
10482
11474
|
}
|
|
10483
11475
|
});
|
|
10484
11476
|
}
|
|
@@ -10586,8 +11578,8 @@ function registerCleanupCommand(parent) {
|
|
|
10586
11578
|
|
|
10587
11579
|
// src/cli/commands/runtime-dashboard.ts
|
|
10588
11580
|
import { lstat as lstat4 } from "fs/promises";
|
|
10589
|
-
import { resolve as
|
|
10590
|
-
import { dump as
|
|
11581
|
+
import { resolve as resolve7 } from "path";
|
|
11582
|
+
import { dump as dump8 } from "js-yaml";
|
|
10591
11583
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
10592
11584
|
function integer3(value, name, minimum = 0) {
|
|
10593
11585
|
const n = Number(value);
|
|
@@ -10649,7 +11641,7 @@ function scanOptions(command) {
|
|
|
10649
11641
|
function registerRuntimeDashboardCommands(runtime) {
|
|
10650
11642
|
const dashboard = runtime.command("dashboard").description("SCM application summaries and daily telemetry (OAuth)");
|
|
10651
11643
|
const sessions = runtime.command("sessions").description("Verified SCM session inventory and drill-down (OAuth)");
|
|
10652
|
-
function
|
|
11644
|
+
function register7(command, prepare) {
|
|
10653
11645
|
command.option("--output <format>", "Output format: pretty, json, yaml").action(async (opts) => {
|
|
10654
11646
|
try {
|
|
10655
11647
|
const read = prepare(opts);
|
|
@@ -10662,7 +11654,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10662
11654
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
10663
11655
|
);
|
|
10664
11656
|
if (opts.outputFile) {
|
|
10665
|
-
const path3 =
|
|
11657
|
+
const path3 = resolve7(text(opts.outputFile, "--output-file"));
|
|
10666
11658
|
const existing = await lstat4(path3).catch((error) => {
|
|
10667
11659
|
if (error.code !== "ENOENT") throw error;
|
|
10668
11660
|
return void 0;
|
|
@@ -10676,9 +11668,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10676
11668
|
numRetries: 0
|
|
10677
11669
|
});
|
|
10678
11670
|
const result = await read(client.dashboard);
|
|
10679
|
-
const output = format === "yaml" ?
|
|
11671
|
+
const output = format === "yaml" ? dump8(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
10680
11672
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
10681
|
-
await writeReportFile(
|
|
11673
|
+
await writeReportFile(resolve7(String(opts.outputFile)), `${output}
|
|
10682
11674
|
`);
|
|
10683
11675
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
10684
11676
|
} else console.log(output);
|
|
@@ -10692,7 +11684,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10692
11684
|
}
|
|
10693
11685
|
});
|
|
10694
11686
|
}
|
|
10695
|
-
|
|
11687
|
+
register7(
|
|
10696
11688
|
pageOptions2(
|
|
10697
11689
|
timeOptions(
|
|
10698
11690
|
dashboard.command("applications").description("One page of application activity"),
|
|
@@ -10721,7 +11713,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10721
11713
|
["application", "application"],
|
|
10722
11714
|
["application-violations", "applicationViolationBreakdown"]
|
|
10723
11715
|
]) {
|
|
10724
|
-
|
|
11716
|
+
register7(
|
|
10725
11717
|
timeOptions(
|
|
10726
11718
|
dashboard.command(name).requiredOption("--app-id <id>", "Application identity").requiredOption("--app-name <name>", "Exact dashboard application name"),
|
|
10727
11719
|
"30",
|
|
@@ -10747,7 +11739,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10747
11739
|
["violations-trend", "applicationsViolationsTrend", false],
|
|
10748
11740
|
["apps-list", "appsList", true]
|
|
10749
11741
|
]) {
|
|
10750
|
-
|
|
11742
|
+
register7(
|
|
10751
11743
|
timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
|
|
10752
11744
|
(opts) => {
|
|
10753
11745
|
const query2 = window(opts);
|
|
@@ -10755,11 +11747,11 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10755
11747
|
}
|
|
10756
11748
|
);
|
|
10757
11749
|
}
|
|
10758
|
-
|
|
11750
|
+
register7(timeOptions(sessions.command("chart")), (opts) => {
|
|
10759
11751
|
const query2 = window(opts);
|
|
10760
11752
|
return (client) => client.sessionsChart(query2);
|
|
10761
11753
|
});
|
|
10762
|
-
|
|
11754
|
+
register7(
|
|
10763
11755
|
pageOptions2(
|
|
10764
11756
|
timeOptions(
|
|
10765
11757
|
sessions.command("list").description("Session entries; --all walks pages with completeness checks")
|
|
@@ -10825,7 +11817,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10825
11817
|
};
|
|
10826
11818
|
}
|
|
10827
11819
|
);
|
|
10828
|
-
|
|
11820
|
+
register7(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
|
|
10829
11821
|
const query2 = {
|
|
10830
11822
|
...identity(opts),
|
|
10831
11823
|
limit: integer3(opts.limit, "--limit", 1),
|
|
@@ -10833,11 +11825,11 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10833
11825
|
};
|
|
10834
11826
|
return (client) => client.session(query2);
|
|
10835
11827
|
});
|
|
10836
|
-
|
|
11828
|
+
register7(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
|
|
10837
11829
|
const query2 = { ...identity(opts), ...scan(opts) };
|
|
10838
11830
|
return (client) => client.sessionTransaction(query2);
|
|
10839
11831
|
});
|
|
10840
|
-
|
|
11832
|
+
register7(
|
|
10841
11833
|
scanOptions(
|
|
10842
11834
|
sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
|
|
10843
11835
|
).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
|
|
@@ -10850,8 +11842,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
10850
11842
|
|
|
10851
11843
|
// src/cli/commands/runtime-report.ts
|
|
10852
11844
|
import { lstat as lstat5 } from "fs/promises";
|
|
10853
|
-
import { resolve as
|
|
10854
|
-
import { randomUUID as
|
|
11845
|
+
import { resolve as resolve8 } from "path";
|
|
11846
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
10855
11847
|
function registerRuntimeReportCommand(runtime) {
|
|
10856
11848
|
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
11849
|
"--output-file <path>",
|
|
@@ -10868,15 +11860,15 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10868
11860
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
10869
11861
|
if (format !== "html" && format !== "markdown")
|
|
10870
11862
|
usageError("Report output must be html or markdown");
|
|
10871
|
-
const
|
|
10872
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
11863
|
+
const maxPages3 = Number(opts.maxPages);
|
|
11864
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages3) || maxPages3 > 100)
|
|
10873
11865
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
10874
11866
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
10875
11867
|
usageError("--title must contain 1\u2013240 characters");
|
|
10876
11868
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
10877
11869
|
usageError("--output-file cannot be empty");
|
|
10878
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
10879
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
11870
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID9().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
11871
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve8(opts.outputFile ?? defaultName);
|
|
10880
11872
|
try {
|
|
10881
11873
|
if (outputPath) {
|
|
10882
11874
|
const existing = await lstat5(outputPath).catch((error) => {
|
|
@@ -10891,7 +11883,7 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10891
11883
|
numRetries: 0
|
|
10892
11884
|
});
|
|
10893
11885
|
ui.status("Collecting daily runtime activity and current configuration (read-only)...");
|
|
10894
|
-
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages:
|
|
11886
|
+
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages3 });
|
|
10895
11887
|
const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
|
|
10896
11888
|
if (outputPath) {
|
|
10897
11889
|
await writeReportFile(outputPath, rendered);
|
|
@@ -11051,7 +12043,7 @@ function registerCreateCommand(parent) {
|
|
|
11051
12043
|
}
|
|
11052
12044
|
|
|
11053
12045
|
// src/cli/commands/topics-eval.ts
|
|
11054
|
-
import { readFile as
|
|
12046
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
11055
12047
|
|
|
11056
12048
|
// src/core/prompt-loader.ts
|
|
11057
12049
|
function parseCsvLine(line) {
|
|
@@ -11194,7 +12186,7 @@ function registerEvalCommand(parent) {
|
|
|
11194
12186
|
resolveDeprecatedAliases(cmd, opts);
|
|
11195
12187
|
try {
|
|
11196
12188
|
const config = await loadConfig();
|
|
11197
|
-
const csvContent = await
|
|
12189
|
+
const csvContent = await readFile11(opts.prompts, "utf-8");
|
|
11198
12190
|
const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
|
|
11199
12191
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
11200
12192
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
@@ -11366,7 +12358,7 @@ function completedBulkResults(state) {
|
|
|
11366
12358
|
}
|
|
11367
12359
|
async function writeBulkResults(outputPath, results) {
|
|
11368
12360
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
11369
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
12361
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
|
|
11370
12362
|
try {
|
|
11371
12363
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
11372
12364
|
encoding: "utf-8",
|
|
@@ -11493,7 +12485,7 @@ function registerRuntimeCommand(program) {
|
|
|
11493
12485
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
11494
12486
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
11495
12487
|
}
|
|
11496
|
-
const raw = await
|
|
12488
|
+
const raw = await readFile12(opts.file, "utf-8");
|
|
11497
12489
|
const prompts = parseInputFile(raw, opts.file);
|
|
11498
12490
|
if (prompts.length === 0) {
|
|
11499
12491
|
usageError("No prompts found in input file");
|
|
@@ -12151,7 +13143,7 @@ function registerRuntimeCommand(program) {
|
|
|
12151
13143
|
}
|
|
12152
13144
|
|
|
12153
13145
|
// src/config/tenant-settings.ts
|
|
12154
|
-
import { randomUUID as
|
|
13146
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
12155
13147
|
import { constants } from "fs";
|
|
12156
13148
|
import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
12157
13149
|
import { dirname as dirname4, join as join4 } from "path";
|
|
@@ -12181,7 +13173,7 @@ async function createManagedTenant(name, config) {
|
|
|
12181
13173
|
throw new Error("Tenant name already exists");
|
|
12182
13174
|
const directory = join4(dirname4(tenantStorePath()), "configs");
|
|
12183
13175
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
12184
|
-
const path3 = join4(directory, `${name}-${
|
|
13176
|
+
const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
|
|
12185
13177
|
const file = await open2(path3, "wx", 384);
|
|
12186
13178
|
try {
|
|
12187
13179
|
const data = Object.fromEntries(
|
|
@@ -12211,11 +13203,11 @@ async function setTenantSetting(name, key, value) {
|
|
|
12211
13203
|
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
12212
13204
|
);
|
|
12213
13205
|
});
|
|
12214
|
-
const temporary = join4(dirname4(path3), `.airs-config-${
|
|
13206
|
+
const temporary = join4(dirname4(path3), `.airs-config-${randomUUID11()}.tmp`);
|
|
12215
13207
|
let created = false;
|
|
12216
13208
|
try {
|
|
12217
|
-
const
|
|
12218
|
-
if (!
|
|
13209
|
+
const stat3 = await lstat6(path3);
|
|
13210
|
+
if (!stat3.isFile() || (stat3.mode & 146) === 0)
|
|
12219
13211
|
throw new Error("Tenant config is not a writable regular file");
|
|
12220
13212
|
await access(path3, constants.W_OK);
|
|
12221
13213
|
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
@@ -12511,7 +13503,7 @@ function buildProgram() {
|
|
|
12511
13503
|
if (root.debug) {
|
|
12512
13504
|
const logPath = join5(
|
|
12513
13505
|
process.cwd(),
|
|
12514
|
-
`debug-api-${Date.now()}-${
|
|
13506
|
+
`debug-api-${Date.now()}-${randomUUID12().slice(0, 8)}.jsonl`
|
|
12515
13507
|
);
|
|
12516
13508
|
try {
|
|
12517
13509
|
let ancestor = actionCommand;
|