@cdot65/prisma-airs-cli 5.4.1 → 5.5.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/README.md +3 -1
- package/dist/{chunk-36Z6IPH6.js → chunk-NKAVUBKI.js} +214 -25
- package/dist/cli/index.js +859 -93
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -13,11 +13,17 @@ import {
|
|
|
13
13
|
collectRedTeamEnvironmentReport,
|
|
14
14
|
collectRuntimeDailyReport,
|
|
15
15
|
computeMetrics,
|
|
16
|
+
createTenant,
|
|
17
|
+
defaultTenantConfigPath,
|
|
18
|
+
deleteTenant,
|
|
19
|
+
expandConfigPath,
|
|
16
20
|
getOrCreateManagementClient,
|
|
17
21
|
inspectConfig,
|
|
18
22
|
loadConfig,
|
|
19
23
|
readBackupDir,
|
|
20
24
|
readBackupFile,
|
|
25
|
+
readTenantConfig,
|
|
26
|
+
readTenantStore,
|
|
21
27
|
renderEnvironmentReportHtml,
|
|
22
28
|
renderEnvironmentReportMarkdown,
|
|
23
29
|
renderRedTeamReportHtml,
|
|
@@ -27,10 +33,11 @@ import {
|
|
|
27
33
|
resolveConfigFilePath,
|
|
28
34
|
resolveOutputDir,
|
|
29
35
|
sanitizeFilename,
|
|
36
|
+
switchTenant,
|
|
30
37
|
validateTopic,
|
|
31
38
|
writeBackupFile,
|
|
32
39
|
writeReportFile
|
|
33
|
-
} from "../chunk-
|
|
40
|
+
} from "../chunk-NKAVUBKI.js";
|
|
34
41
|
|
|
35
42
|
// src/cli/index.ts
|
|
36
43
|
import "dotenv/config";
|
|
@@ -49,7 +56,7 @@ function installProcessGuards() {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
// src/cli/program.ts
|
|
52
|
-
import { randomUUID as
|
|
59
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
53
60
|
import { readFileSync as readFileSync4 } from "fs";
|
|
54
61
|
import { dirname as dirname4, join as join4 } from "path";
|
|
55
62
|
import { fileURLToPath } from "url";
|
|
@@ -132,8 +139,8 @@ function modelSecurityClientOptions(config) {
|
|
|
132
139
|
|
|
133
140
|
// src/reports/agentguard.ts
|
|
134
141
|
async function collectAgentGuardReport(client, options = {}) {
|
|
135
|
-
const
|
|
136
|
-
if (!Number.isSafeInteger(
|
|
142
|
+
const maxPages2 = options.maxPages ?? 40;
|
|
143
|
+
if (!Number.isSafeInteger(maxPages2) || maxPages2 < 1 || maxPages2 > 100)
|
|
137
144
|
throw new Error("maxPages must be between 1 and 100");
|
|
138
145
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
139
146
|
const end = now();
|
|
@@ -195,7 +202,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
195
202
|
const seen = /* @__PURE__ */ new Set();
|
|
196
203
|
let total;
|
|
197
204
|
try {
|
|
198
|
-
for (let page = 0; page <
|
|
205
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
199
206
|
const r = await read(rows.length);
|
|
200
207
|
s.pages++;
|
|
201
208
|
if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
|
|
@@ -212,7 +219,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
212
219
|
if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
|
|
213
220
|
if (!r.rows.length) throw new Error();
|
|
214
221
|
}
|
|
215
|
-
s.notes.push(`Reached the ${
|
|
222
|
+
s.notes.push(`Reached the ${maxPages2}-page budget.`);
|
|
216
223
|
throw new Error();
|
|
217
224
|
} catch (error) {
|
|
218
225
|
if (!rows.length) throw error;
|
|
@@ -375,7 +382,7 @@ async function resolveOutput(command, opts, resolution = {}) {
|
|
|
375
382
|
const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
|
|
376
383
|
let configured;
|
|
377
384
|
try {
|
|
378
|
-
configured = (await loadConfig()).defaultOutput;
|
|
385
|
+
configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
|
|
379
386
|
} catch (error) {
|
|
380
387
|
if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
|
|
381
388
|
else throw error;
|
|
@@ -2594,13 +2601,13 @@ function statusColor4(status) {
|
|
|
2594
2601
|
function renderRuntimeConfigHeader() {
|
|
2595
2602
|
ui.header("Prisma AIRS \u2014 Runtime Configuration", "Security profile and topic management");
|
|
2596
2603
|
}
|
|
2597
|
-
function renderProfileList(
|
|
2598
|
-
if (
|
|
2604
|
+
function renderProfileList(profiles2, format = "pretty") {
|
|
2605
|
+
if (profiles2.length === 0) {
|
|
2599
2606
|
ui.emptyList("profiles");
|
|
2600
2607
|
return;
|
|
2601
2608
|
}
|
|
2602
2609
|
if (format !== "pretty") {
|
|
2603
|
-
const rows =
|
|
2610
|
+
const rows = profiles2.map((p) => ({
|
|
2604
2611
|
id: p.profileId,
|
|
2605
2612
|
name: p.profileName,
|
|
2606
2613
|
status: p.active ? "active" : "inactive",
|
|
@@ -2616,7 +2623,7 @@ function renderProfileList(profiles, format = "pretty") {
|
|
|
2616
2623
|
return;
|
|
2617
2624
|
}
|
|
2618
2625
|
ui.section("Security Profiles:");
|
|
2619
|
-
for (const p of
|
|
2626
|
+
for (const p of profiles2) {
|
|
2620
2627
|
ui.dim(p.profileId);
|
|
2621
2628
|
const status = p.active ? chalk9.green("active") : chalk9.yellow("inactive");
|
|
2622
2629
|
const rev = p.revision != null ? chalk9.dim(` rev:${p.revision}`) : "";
|
|
@@ -2704,13 +2711,13 @@ function renderCleanupResult(results, format = "pretty") {
|
|
|
2704
2711
|
}
|
|
2705
2712
|
console.log();
|
|
2706
2713
|
}
|
|
2707
|
-
function renderTopicList(
|
|
2708
|
-
if (
|
|
2714
|
+
function renderTopicList(topics2, format = "pretty") {
|
|
2715
|
+
if (topics2.length === 0) {
|
|
2709
2716
|
ui.emptyList("topics");
|
|
2710
2717
|
return;
|
|
2711
2718
|
}
|
|
2712
2719
|
if (format !== "pretty") {
|
|
2713
|
-
const rows =
|
|
2720
|
+
const rows = topics2.map((t) => ({
|
|
2714
2721
|
id: t.topic_id ?? "",
|
|
2715
2722
|
name: t.topic_name,
|
|
2716
2723
|
revision: t.revision ?? "",
|
|
@@ -2731,7 +2738,7 @@ function renderTopicList(topics, format = "pretty") {
|
|
|
2731
2738
|
return;
|
|
2732
2739
|
}
|
|
2733
2740
|
ui.section("Custom Topics:");
|
|
2734
|
-
for (const t of
|
|
2741
|
+
for (const t of topics2) {
|
|
2735
2742
|
ui.dim(String(t.topic_id));
|
|
2736
2743
|
const rev = t.revision != null ? chalk9.dim(` rev:${t.revision}`) : "";
|
|
2737
2744
|
const desc = t.description ? chalk9.dim(` \u2014 ${t.description.slice(0, 80)}`) : "";
|
|
@@ -2933,13 +2940,13 @@ function renderCustomerAppConsumption(data, format = "pretty") {
|
|
|
2933
2940
|
}
|
|
2934
2941
|
console.log();
|
|
2935
2942
|
}
|
|
2936
|
-
function renderDeploymentProfileList(
|
|
2937
|
-
if (
|
|
2943
|
+
function renderDeploymentProfileList(profiles2, format = "pretty") {
|
|
2944
|
+
if (profiles2.length === 0) {
|
|
2938
2945
|
ui.emptyList("deployment profiles");
|
|
2939
2946
|
return;
|
|
2940
2947
|
}
|
|
2941
2948
|
if (format !== "pretty") {
|
|
2942
|
-
const rows =
|
|
2949
|
+
const rows = profiles2.map((p) => ({
|
|
2943
2950
|
name: p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "",
|
|
2944
2951
|
status: p.raw.status ?? "",
|
|
2945
2952
|
authCode: p.raw.auth_code ?? ""
|
|
@@ -2958,7 +2965,7 @@ function renderDeploymentProfileList(profiles, format = "pretty") {
|
|
|
2958
2965
|
return;
|
|
2959
2966
|
}
|
|
2960
2967
|
ui.section("Deployment Profiles:");
|
|
2961
|
-
for (const p of
|
|
2968
|
+
for (const p of profiles2) {
|
|
2962
2969
|
const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
|
|
2963
2970
|
const status = p.raw.status;
|
|
2964
2971
|
const authCode = p.raw.auth_code;
|
|
@@ -3480,7 +3487,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3480
3487
|
const outputError = (error) => controller.abort(error);
|
|
3481
3488
|
let stream;
|
|
3482
3489
|
let failed = false;
|
|
3483
|
-
let
|
|
3490
|
+
let failure2;
|
|
3484
3491
|
try {
|
|
3485
3492
|
const format = await resolveOutput(command, opts, { allowed: ["pretty", "json", "yaml"] });
|
|
3486
3493
|
const config = await loadConfig({ aiGwInferenceEndpoint: opts.endpoint });
|
|
@@ -3507,8 +3514,8 @@ function registerAiGatewayInference(parent) {
|
|
|
3507
3514
|
process.once("SIGINT", interrupt);
|
|
3508
3515
|
process.once("SIGTERM", interrupt);
|
|
3509
3516
|
process.stdout.on("error", outputError);
|
|
3510
|
-
const write = (text2) => new Promise((
|
|
3511
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3517
|
+
const write = (text2) => new Promise((resolve8, reject) => {
|
|
3518
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve8());
|
|
3512
3519
|
});
|
|
3513
3520
|
const requestOptions = { signal: controller.signal };
|
|
3514
3521
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -3543,7 +3550,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3543
3550
|
}
|
|
3544
3551
|
} catch (error) {
|
|
3545
3552
|
failed = true;
|
|
3546
|
-
|
|
3553
|
+
failure2 = error;
|
|
3547
3554
|
} finally {
|
|
3548
3555
|
await stream?.cancel();
|
|
3549
3556
|
controller.abort();
|
|
@@ -3551,10 +3558,10 @@ function registerAiGatewayInference(parent) {
|
|
|
3551
3558
|
process.removeListener("SIGTERM", interrupt);
|
|
3552
3559
|
process.stdout.removeListener("error", outputError);
|
|
3553
3560
|
}
|
|
3554
|
-
if (failed &&
|
|
3555
|
-
if (
|
|
3556
|
-
usageError(
|
|
3557
|
-
fail(
|
|
3561
|
+
if (failed && failure2?.code !== "EPIPE") {
|
|
3562
|
+
if (failure2?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
|
|
3563
|
+
usageError(failure2 instanceof Error ? failure2.message : "Invalid inference options");
|
|
3564
|
+
fail(failure2);
|
|
3558
3565
|
}
|
|
3559
3566
|
});
|
|
3560
3567
|
}
|
|
@@ -3882,20 +3889,20 @@ function renderGatewayDetail(value, format) {
|
|
|
3882
3889
|
);
|
|
3883
3890
|
if (rendered) console.log(rendered);
|
|
3884
3891
|
}
|
|
3885
|
-
async function runList(command, opts, label,
|
|
3892
|
+
async function runList(command, opts, label, load3) {
|
|
3886
3893
|
try {
|
|
3887
3894
|
const format = await resolveOutput(command, opts);
|
|
3888
3895
|
const client = await createAiGatewayClient();
|
|
3889
|
-
renderGatewayList(responseItems(await
|
|
3896
|
+
renderGatewayList(responseItems(await load3(client)), format, label);
|
|
3890
3897
|
} catch (error) {
|
|
3891
3898
|
failAiGateway(error);
|
|
3892
3899
|
}
|
|
3893
3900
|
}
|
|
3894
|
-
async function runDetail(command, opts,
|
|
3901
|
+
async function runDetail(command, opts, load3) {
|
|
3895
3902
|
try {
|
|
3896
3903
|
const format = await resolveOutput(command, opts);
|
|
3897
3904
|
const client = await createAiGatewayClient();
|
|
3898
|
-
renderGatewayDetail(await
|
|
3905
|
+
renderGatewayDetail(await load3(client), format);
|
|
3899
3906
|
} catch (error) {
|
|
3900
3907
|
failAiGateway(error);
|
|
3901
3908
|
}
|
|
@@ -5230,8 +5237,8 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5230
5237
|
const iso = z2.string().datetime({ offset: true });
|
|
5231
5238
|
if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
|
|
5232
5239
|
usageError("Provide a valid increasing ISO-8601 window with timezone");
|
|
5233
|
-
const
|
|
5234
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
5240
|
+
const maxPages2 = Number(opts.maxPages);
|
|
5241
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
5235
5242
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
5236
5243
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
5237
5244
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -5255,7 +5262,7 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5255
5262
|
ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
|
|
5256
5263
|
const report = await collectGatewayEnvironmentReport(client, {
|
|
5257
5264
|
title: opts.title,
|
|
5258
|
-
maxPages,
|
|
5265
|
+
maxPages: maxPages2,
|
|
5259
5266
|
workspace: opts.workspace,
|
|
5260
5267
|
tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
|
|
5261
5268
|
start,
|
|
@@ -5886,8 +5893,8 @@ async function withTimeout(promise, ms) {
|
|
|
5886
5893
|
try {
|
|
5887
5894
|
return await Promise.race([
|
|
5888
5895
|
promise,
|
|
5889
|
-
new Promise((
|
|
5890
|
-
timer = setTimeout(() =>
|
|
5896
|
+
new Promise((resolve8) => {
|
|
5897
|
+
timer = setTimeout(() => resolve8(TIMED_OUT), ms);
|
|
5891
5898
|
})
|
|
5892
5899
|
]);
|
|
5893
5900
|
} finally {
|
|
@@ -6052,8 +6059,8 @@ async function defaultMgmtProbe() {
|
|
|
6052
6059
|
tsgId: config.mgmtTsgId,
|
|
6053
6060
|
tokenEndpoint: config.mgmtTokenEndpoint
|
|
6054
6061
|
});
|
|
6055
|
-
const
|
|
6056
|
-
return
|
|
6062
|
+
const topics2 = await service.listTopics();
|
|
6063
|
+
return topics2.length;
|
|
6057
6064
|
}
|
|
6058
6065
|
async function defaultAiGwProbe() {
|
|
6059
6066
|
const config = await loadConfig();
|
|
@@ -6172,12 +6179,12 @@ function hasBin(bin) {
|
|
|
6172
6179
|
}
|
|
6173
6180
|
}
|
|
6174
6181
|
function run2(bin, args, label) {
|
|
6175
|
-
return new Promise((
|
|
6182
|
+
return new Promise((resolve8, reject) => {
|
|
6176
6183
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
6177
6184
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
6178
6185
|
child.on("close", (code) => {
|
|
6179
6186
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
6180
|
-
else
|
|
6187
|
+
else resolve8();
|
|
6181
6188
|
});
|
|
6182
6189
|
});
|
|
6183
6190
|
}
|
|
@@ -6787,8 +6794,8 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6787
6794
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6788
6795
|
if (format !== "html" && format !== "markdown")
|
|
6789
6796
|
usageError("Report output must be html or markdown");
|
|
6790
|
-
const
|
|
6791
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
6797
|
+
const maxPages2 = Number(opts.maxPages);
|
|
6798
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
6792
6799
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
6793
6800
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
6794
6801
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -6812,7 +6819,7 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6812
6819
|
ui.status("Collecting Red Team statistics and current configuration (read-only)...");
|
|
6813
6820
|
const report = await collectRedTeamEnvironmentReport(client, {
|
|
6814
6821
|
title: opts.title,
|
|
6815
|
-
maxPages
|
|
6822
|
+
maxPages: maxPages2
|
|
6816
6823
|
});
|
|
6817
6824
|
const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
|
|
6818
6825
|
if (outputPath) {
|
|
@@ -8071,9 +8078,9 @@ function registerRedteamCommand(program) {
|
|
|
8071
8078
|
}
|
|
8072
8079
|
|
|
8073
8080
|
// src/cli/commands/runtime.ts
|
|
8074
|
-
import { randomUUID as
|
|
8081
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
8075
8082
|
import * as fs5 from "fs";
|
|
8076
|
-
import { readFile as
|
|
8083
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
8077
8084
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
8078
8085
|
import chalk11 from "chalk";
|
|
8079
8086
|
|
|
@@ -8137,9 +8144,9 @@ function buildDataProtection(flags) {
|
|
|
8137
8144
|
let hasAny = false;
|
|
8138
8145
|
if (flags.dlpAction) {
|
|
8139
8146
|
const dld = { action: flags.dlpAction };
|
|
8140
|
-
const
|
|
8141
|
-
if (
|
|
8142
|
-
dld.member =
|
|
8147
|
+
const profiles2 = parseList(flags.dlpProfiles);
|
|
8148
|
+
if (profiles2) {
|
|
8149
|
+
dld.member = profiles2.map((text2) => ({ text: text2 }));
|
|
8143
8150
|
}
|
|
8144
8151
|
if (flags.maskDataInline != null) {
|
|
8145
8152
|
dld["mask-data-inline"] = flags.maskDataInline;
|
|
@@ -9525,11 +9532,655 @@ function registerDlpCommands(runtime) {
|
|
|
9525
9532
|
register3(dlp);
|
|
9526
9533
|
}
|
|
9527
9534
|
|
|
9535
|
+
// src/cli/commands/profile-transfer.ts
|
|
9536
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9537
|
+
import { readFile as readFile9, stat } from "fs/promises";
|
|
9538
|
+
import { extname, resolve as resolve5 } from "path";
|
|
9539
|
+
import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9540
|
+
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
9541
|
+
import { ZodError } from "zod";
|
|
9542
|
+
|
|
9543
|
+
// src/backup/runtime-profiles.ts
|
|
9544
|
+
import {
|
|
9545
|
+
CreateSecurityProfileRequestSchema,
|
|
9546
|
+
CustomTopicSchema,
|
|
9547
|
+
PolicySchema,
|
|
9548
|
+
SecurityProfileSchema
|
|
9549
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
9550
|
+
import { z as z5 } from "zod";
|
|
9551
|
+
var RuntimeProfilesBackupSchema = z5.object({
|
|
9552
|
+
version: z5.literal(1),
|
|
9553
|
+
resourceType: z5.literal("runtime-security-profiles"),
|
|
9554
|
+
exportedAt: z5.string().datetime({ offset: true }),
|
|
9555
|
+
source: z5.object({ tsgId: z5.string().min(1) }).strict(),
|
|
9556
|
+
profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
|
|
9557
|
+
topics: z5.array(CustomTopicSchema).max(1e4)
|
|
9558
|
+
}).strict();
|
|
9559
|
+
function object(value) {
|
|
9560
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9561
|
+
}
|
|
9562
|
+
function pagesLimit(value) {
|
|
9563
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
|
|
9564
|
+
throw new Error("maxPages must be an integer from 1 to 1000");
|
|
9565
|
+
}
|
|
9566
|
+
async function offsetPages(fetch, maxPages2) {
|
|
9567
|
+
pagesLimit(maxPages2);
|
|
9568
|
+
const items = [];
|
|
9569
|
+
let offset = 0;
|
|
9570
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
9571
|
+
const result = await fetch(offset);
|
|
9572
|
+
items.push(...result.items);
|
|
9573
|
+
if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
|
|
9574
|
+
if (result.next === void 0 || result.next === 0) return items;
|
|
9575
|
+
if (!result.items.length || !Number.isSafeInteger(result.next) || result.next <= offset)
|
|
9576
|
+
throw new Error("Inventory pagination did not advance");
|
|
9577
|
+
offset = result.next;
|
|
9578
|
+
}
|
|
9579
|
+
throw new Error("Inventory is incomplete: increase --max-pages before exporting or restoring");
|
|
9580
|
+
}
|
|
9581
|
+
function latestProfiles(profiles2) {
|
|
9582
|
+
const names = /* @__PURE__ */ new Map();
|
|
9583
|
+
const revisions = /* @__PURE__ */ new Set();
|
|
9584
|
+
for (const profile of profiles2) {
|
|
9585
|
+
assertName(profile.profile_name);
|
|
9586
|
+
const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
|
|
9587
|
+
if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
|
|
9588
|
+
revisions.add(identity2);
|
|
9589
|
+
const prior = names.get(profile.profile_name);
|
|
9590
|
+
if (prior && prior.profile_id !== profile.profile_id)
|
|
9591
|
+
throw new Error(`Ambiguous profile name: ${profile.profile_name}`);
|
|
9592
|
+
if (!prior || (profile.revision ?? 0) > (prior.revision ?? 0))
|
|
9593
|
+
names.set(profile.profile_name, profile);
|
|
9594
|
+
}
|
|
9595
|
+
return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
|
|
9596
|
+
}
|
|
9597
|
+
function assertName(name) {
|
|
9598
|
+
if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9599
|
+
throw new Error("Profile/topic names must be nonempty and contain no control characters");
|
|
9600
|
+
}
|
|
9601
|
+
function assertTenant(profiles2, tsgId) {
|
|
9602
|
+
if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
|
|
9603
|
+
throw new Error("Destination/source profile tenant identity does not match the selected TSG");
|
|
9604
|
+
}
|
|
9605
|
+
async function profiles(api, maxPages2) {
|
|
9606
|
+
return latestProfiles(
|
|
9607
|
+
await offsetPages(async (offset) => {
|
|
9608
|
+
const page = await api.profiles.list({ offset, limit: 100, latest: true });
|
|
9609
|
+
return { items: page.ai_profiles, next: page.next_offset };
|
|
9610
|
+
}, maxPages2)
|
|
9611
|
+
);
|
|
9612
|
+
}
|
|
9613
|
+
async function topics(api, maxPages2) {
|
|
9614
|
+
return offsetPages(async (offset) => {
|
|
9615
|
+
const page = await api.topics.list({ offset, limit: 100 });
|
|
9616
|
+
return { items: page.custom_topics, next: page.next_offset };
|
|
9617
|
+
}, maxPages2);
|
|
9618
|
+
}
|
|
9619
|
+
function topicReferences(policy) {
|
|
9620
|
+
const refs = [];
|
|
9621
|
+
const visit = (value) => {
|
|
9622
|
+
if (Array.isArray(value)) {
|
|
9623
|
+
value.forEach(visit);
|
|
9624
|
+
return;
|
|
9625
|
+
}
|
|
9626
|
+
if (!object(value)) return;
|
|
9627
|
+
if ("topic_id" in value) {
|
|
9628
|
+
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9629
|
+
throw new Error("Invalid topic identity in profile policy");
|
|
9630
|
+
refs.push(value);
|
|
9631
|
+
}
|
|
9632
|
+
Object.values(value).forEach(visit);
|
|
9633
|
+
};
|
|
9634
|
+
visit(policy);
|
|
9635
|
+
return refs;
|
|
9636
|
+
}
|
|
9637
|
+
function topicKey(ref) {
|
|
9638
|
+
return JSON.stringify([ref.topic_id, ref.revision]);
|
|
9639
|
+
}
|
|
9640
|
+
async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
9641
|
+
if (!tsgId) throw new Error("Source TSG ID is required");
|
|
9642
|
+
const maxPages2 = options.maxPages ?? 100;
|
|
9643
|
+
let selected = await profiles(api, maxPages2);
|
|
9644
|
+
assertTenant(selected, tsgId);
|
|
9645
|
+
if (options.profile)
|
|
9646
|
+
selected = selected.filter(
|
|
9647
|
+
(p) => p.profile_id === options.profile || p.profile_name === options.profile
|
|
9648
|
+
);
|
|
9649
|
+
if (!selected.length) throw new Error("No matching profiles to back up");
|
|
9650
|
+
if (options.profile && selected.length !== 1) throw new Error("Ambiguous profile selector");
|
|
9651
|
+
for (const profile of selected) {
|
|
9652
|
+
if (!profile.policy) throw new Error(`Profile has no policy: ${profile.profile_name}`);
|
|
9653
|
+
PolicySchema.parse(profile.policy);
|
|
9654
|
+
}
|
|
9655
|
+
const refs = selected.flatMap((profile) => topicReferences(profile.policy));
|
|
9656
|
+
const definitions = refs.length ? await topics(api, maxPages2) : [];
|
|
9657
|
+
const needed = /* @__PURE__ */ new Map();
|
|
9658
|
+
for (const ref of refs) {
|
|
9659
|
+
const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
|
|
9660
|
+
if (matches.length !== 1 || matches[0].topic_name !== ref.topic_name)
|
|
9661
|
+
throw new Error(
|
|
9662
|
+
`Cannot export the exact referenced topic revision: ${ref.topic_name} revision ${ref.revision}`
|
|
9663
|
+
);
|
|
9664
|
+
needed.set(topicKey(ref), matches[0]);
|
|
9665
|
+
}
|
|
9666
|
+
return RuntimeProfilesBackupSchema.parse({
|
|
9667
|
+
version: 1,
|
|
9668
|
+
resourceType: "runtime-security-profiles",
|
|
9669
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9670
|
+
source: { tsgId },
|
|
9671
|
+
profiles: selected,
|
|
9672
|
+
topics: [...needed.values()]
|
|
9673
|
+
});
|
|
9674
|
+
}
|
|
9675
|
+
function sameTopic(a, b) {
|
|
9676
|
+
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9677
|
+
}
|
|
9678
|
+
function dlpReferences(policy) {
|
|
9679
|
+
if (!object(policy)) return [];
|
|
9680
|
+
const refs = [];
|
|
9681
|
+
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9682
|
+
if (!object(entry) || typeof entry.name !== "string")
|
|
9683
|
+
throw new Error("Invalid embedded DLP reference");
|
|
9684
|
+
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9685
|
+
}
|
|
9686
|
+
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9687
|
+
if (!object(entry) || !object(entry["model-configuration"])) continue;
|
|
9688
|
+
const protection = entry["model-configuration"]["data-protection"];
|
|
9689
|
+
if (!object(protection) || !object(protection["data-leak-detection"])) continue;
|
|
9690
|
+
const members = protection["data-leak-detection"].member;
|
|
9691
|
+
if (!Array.isArray(members)) continue;
|
|
9692
|
+
for (const member of members) {
|
|
9693
|
+
if (!object(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9694
|
+
refs.push({ value: member, name: member.text, kind: "member" });
|
|
9695
|
+
}
|
|
9696
|
+
}
|
|
9697
|
+
return refs;
|
|
9698
|
+
}
|
|
9699
|
+
function assertKnownPolicyReferences(policy) {
|
|
9700
|
+
const known = new Map([
|
|
9701
|
+
...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
|
|
9702
|
+
...dlpReferences(policy).map((ref) => [
|
|
9703
|
+
ref.value,
|
|
9704
|
+
ref.kind === "embedded" ? ["id", "uuid"] : ["id"]
|
|
9705
|
+
])
|
|
9706
|
+
]);
|
|
9707
|
+
const visit = (value, path3) => {
|
|
9708
|
+
if (Array.isArray(value)) {
|
|
9709
|
+
value.forEach((v, i) => {
|
|
9710
|
+
visit(v, `${path3}[${i}]`);
|
|
9711
|
+
});
|
|
9712
|
+
return;
|
|
9713
|
+
}
|
|
9714
|
+
if (!object(value)) return;
|
|
9715
|
+
for (const [key, child] of Object.entries(value)) {
|
|
9716
|
+
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9717
|
+
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
9718
|
+
visit(child, `${path3}.${key}`);
|
|
9719
|
+
}
|
|
9720
|
+
};
|
|
9721
|
+
visit(policy, "policy");
|
|
9722
|
+
}
|
|
9723
|
+
async function dataProfiles(api, maxPages2) {
|
|
9724
|
+
const items = [];
|
|
9725
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
9726
|
+
const result = await api.dataProfiles.list({ page, size: 100 });
|
|
9727
|
+
items.push(...result.content);
|
|
9728
|
+
if (items.length > 1e4) throw new Error("DLP inventory exceeds the safety limit");
|
|
9729
|
+
const last = result.last ?? (result.totalPages !== void 0 ? page + 1 >= result.totalPages : result.content.length < 100);
|
|
9730
|
+
if (last) return items;
|
|
9731
|
+
if (!result.content.length) throw new Error("DLP inventory pagination did not advance");
|
|
9732
|
+
}
|
|
9733
|
+
throw new Error("DLP inventory is incomplete; increase --max-pages");
|
|
9734
|
+
}
|
|
9735
|
+
async function planRuntimeProfilesRestore(api, input, destinationTsgId, options = {}) {
|
|
9736
|
+
const backup = RuntimeProfilesBackupSchema.parse(input);
|
|
9737
|
+
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9738
|
+
const maxPages2 = options.maxPages ?? 100;
|
|
9739
|
+
pagesLimit(maxPages2);
|
|
9740
|
+
const prefix = options.namePrefix ?? "";
|
|
9741
|
+
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9742
|
+
throw new Error("Invalid --name-prefix");
|
|
9743
|
+
const conflict = options.onConflict ?? "error";
|
|
9744
|
+
if (!["error", "skip", "update"].includes(conflict)) throw new Error("Invalid conflict policy");
|
|
9745
|
+
const selected = latestProfiles(backup.profiles);
|
|
9746
|
+
assertTenant(selected, backup.source.tsgId);
|
|
9747
|
+
if (selected.length !== backup.profiles.length)
|
|
9748
|
+
throw new Error("Backup contains duplicate profile names/revisions");
|
|
9749
|
+
const crossTenant = backup.source.tsgId !== destinationTsgId;
|
|
9750
|
+
for (const profile of selected) {
|
|
9751
|
+
if (!profile.policy) throw new Error("Backup profile has no policy");
|
|
9752
|
+
CreateSecurityProfileRequestSchema.parse({
|
|
9753
|
+
profile_name: `${prefix}${profile.profile_name}`,
|
|
9754
|
+
active: profile.active,
|
|
9755
|
+
policy: profile.policy
|
|
9756
|
+
});
|
|
9757
|
+
if (crossTenant) assertKnownPolicyReferences(profile.policy);
|
|
9758
|
+
}
|
|
9759
|
+
const existing = await profiles(api, maxPages2);
|
|
9760
|
+
assertTenant(existing, destinationTsgId);
|
|
9761
|
+
const profilePlans = selected.map((source) => {
|
|
9762
|
+
const name = `${prefix}${source.profile_name}`;
|
|
9763
|
+
const match = existing.find((p) => p.profile_name === name);
|
|
9764
|
+
if (match && conflict === "error")
|
|
9765
|
+
throw new Error(
|
|
9766
|
+
`Destination profile already exists: ${name}; choose --on-conflict skip or update`
|
|
9767
|
+
);
|
|
9768
|
+
if (match && !match.profile_id) throw new Error("Destination profile has no identity");
|
|
9769
|
+
return {
|
|
9770
|
+
source: structuredClone(source),
|
|
9771
|
+
name,
|
|
9772
|
+
existing: match,
|
|
9773
|
+
action: match ? conflict === "skip" ? "skip" : "update" : "create"
|
|
9774
|
+
};
|
|
9775
|
+
});
|
|
9776
|
+
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
9777
|
+
const refs = changing.flatMap((p) => topicReferences(p.source.policy));
|
|
9778
|
+
const targetTopics = refs.length ? await topics(api, maxPages2) : [];
|
|
9779
|
+
const topicPlans = /* @__PURE__ */ new Map();
|
|
9780
|
+
const topicNames = /* @__PURE__ */ new Map();
|
|
9781
|
+
for (const ref of refs) {
|
|
9782
|
+
const key = topicKey(ref);
|
|
9783
|
+
if (topicPlans.has(key)) continue;
|
|
9784
|
+
const definitions = backup.topics.filter(
|
|
9785
|
+
(topic) => topicKey(topic) === key && topic.topic_name === ref.topic_name
|
|
9786
|
+
);
|
|
9787
|
+
if (definitions.length !== 1)
|
|
9788
|
+
throw new Error(`Backup lacks an unambiguous topic definition: ${ref.topic_name}`);
|
|
9789
|
+
const source = definitions[0];
|
|
9790
|
+
const name = `${prefix}${source.topic_name}`;
|
|
9791
|
+
assertName(name);
|
|
9792
|
+
if (topicNames.has(name) && topicNames.get(name) !== key)
|
|
9793
|
+
throw new Error(
|
|
9794
|
+
`Multiple source revisions use topic name ${name}; split the backup by profile/revision before restoring`
|
|
9795
|
+
);
|
|
9796
|
+
topicNames.set(name, key);
|
|
9797
|
+
const matches = targetTopics.filter((t) => t.topic_name === name).sort((a, b) => b.revision - a.revision);
|
|
9798
|
+
const match = matches[0];
|
|
9799
|
+
if (new Set(matches.map((t) => t.topic_id)).size > 1)
|
|
9800
|
+
throw new Error(`Ambiguous destination topic name: ${name}`);
|
|
9801
|
+
if (match && (!match.topic_id || !sameTopic(source, match)))
|
|
9802
|
+
throw new Error(
|
|
9803
|
+
`Destination topic differs: ${name}; use --name-prefix to avoid changing a shared topic`
|
|
9804
|
+
);
|
|
9805
|
+
topicPlans.set(key, { key, name, source, existing: match });
|
|
9806
|
+
}
|
|
9807
|
+
const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9808
|
+
const names = [...new Set(dlpRefs.map((ref) => ref.name))];
|
|
9809
|
+
for (const name of names)
|
|
9810
|
+
if (!options.dlpMap?.[name])
|
|
9811
|
+
throw new Error(
|
|
9812
|
+
`Cross-tenant DLP reference requires explicit --dlp-map ${name}=<destination-name>; DLP resources are not cloned`
|
|
9813
|
+
);
|
|
9814
|
+
const catalog = names.length ? await dataProfiles(api, maxPages2) : [];
|
|
9815
|
+
const dlpMappings = names.map((name) => {
|
|
9816
|
+
const target = options.dlpMap?.[name];
|
|
9817
|
+
const matches = catalog.filter((p) => p.name === target && p.profile_status !== "deleted");
|
|
9818
|
+
if (matches.length !== 1 || !matches[0].id)
|
|
9819
|
+
throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
|
|
9820
|
+
if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
|
|
9821
|
+
throw new Error(`Destination DLP version is unknown: ${target}`);
|
|
9822
|
+
return {
|
|
9823
|
+
source: name,
|
|
9824
|
+
destination: String(target),
|
|
9825
|
+
id: matches[0].id,
|
|
9826
|
+
version: matches[0].version ?? void 0
|
|
9827
|
+
};
|
|
9828
|
+
});
|
|
9829
|
+
return {
|
|
9830
|
+
sourceTsgId: backup.source.tsgId,
|
|
9831
|
+
destinationTsgId,
|
|
9832
|
+
profiles: profilePlans,
|
|
9833
|
+
topics: [...topicPlans.values()],
|
|
9834
|
+
dlpMappings,
|
|
9835
|
+
maxPages: maxPages2
|
|
9836
|
+
};
|
|
9837
|
+
}
|
|
9838
|
+
function canonical(value) {
|
|
9839
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object(v) ? Object.fromEntries(
|
|
9840
|
+
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9841
|
+
) : v;
|
|
9842
|
+
return JSON.stringify(order(value));
|
|
9843
|
+
}
|
|
9844
|
+
function unchanged(current, prior) {
|
|
9845
|
+
return Boolean(
|
|
9846
|
+
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonical(current.policy) === canonical(prior.policy)
|
|
9847
|
+
);
|
|
9848
|
+
}
|
|
9849
|
+
async function restoreRuntimeProfiles(api, plan) {
|
|
9850
|
+
const result = {
|
|
9851
|
+
sourceTsgId: plan.sourceTsgId,
|
|
9852
|
+
destinationTsgId: plan.destinationTsgId,
|
|
9853
|
+
complete: false,
|
|
9854
|
+
topics: [],
|
|
9855
|
+
profiles: []
|
|
9856
|
+
};
|
|
9857
|
+
try {
|
|
9858
|
+
const latest = await profiles(api, plan.maxPages);
|
|
9859
|
+
assertTenant(latest, plan.destinationTsgId);
|
|
9860
|
+
if (plan.dlpMappings.length) {
|
|
9861
|
+
const catalog = await dataProfiles(api, plan.maxPages);
|
|
9862
|
+
for (const mapping of plan.dlpMappings) {
|
|
9863
|
+
const matches = catalog.filter(
|
|
9864
|
+
(p) => p.name === mapping.destination && p.profile_status !== "deleted"
|
|
9865
|
+
);
|
|
9866
|
+
if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
|
|
9867
|
+
throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
|
|
9868
|
+
}
|
|
9869
|
+
}
|
|
9870
|
+
for (const item of plan.profiles) {
|
|
9871
|
+
if (item.action === "skip") continue;
|
|
9872
|
+
const current = latest.find((p) => p.profile_name === item.name);
|
|
9873
|
+
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9874
|
+
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
9875
|
+
}
|
|
9876
|
+
const mappedTopics = /* @__PURE__ */ new Map();
|
|
9877
|
+
for (const item of plan.topics) {
|
|
9878
|
+
let topic = item.existing;
|
|
9879
|
+
const inventory2 = await topics(api, plan.maxPages);
|
|
9880
|
+
if (topic) {
|
|
9881
|
+
const current = inventory2.find(
|
|
9882
|
+
(t) => t.topic_id === topic?.topic_id && t.revision === topic?.revision
|
|
9883
|
+
);
|
|
9884
|
+
if (!current || !sameTopic(current, item.source))
|
|
9885
|
+
throw new Error(`Destination topic changed after planning: ${item.name}`);
|
|
9886
|
+
} else {
|
|
9887
|
+
if (inventory2.some((t) => t.topic_name === item.name))
|
|
9888
|
+
throw new Error(`Destination topic changed after planning: ${item.name}`);
|
|
9889
|
+
topic = await api.topics.create({
|
|
9890
|
+
topic_name: item.name,
|
|
9891
|
+
description: item.source.description,
|
|
9892
|
+
examples: item.source.examples,
|
|
9893
|
+
active: item.source.active
|
|
9894
|
+
});
|
|
9895
|
+
}
|
|
9896
|
+
if (!topic.topic_id || topic.topic_name !== item.name || !Number.isSafeInteger(topic.revision) || !sameTopic(topic, item.source))
|
|
9897
|
+
throw new Error(`Topic response did not verify: ${item.name}`);
|
|
9898
|
+
mappedTopics.set(item.key, topic);
|
|
9899
|
+
result.topics.push({
|
|
9900
|
+
name: item.name,
|
|
9901
|
+
id: topic.topic_id,
|
|
9902
|
+
action: item.existing ? "reused" : "created"
|
|
9903
|
+
});
|
|
9904
|
+
}
|
|
9905
|
+
for (const item of plan.profiles) {
|
|
9906
|
+
if (item.action === "skip") {
|
|
9907
|
+
result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
|
|
9908
|
+
continue;
|
|
9909
|
+
}
|
|
9910
|
+
const policy = structuredClone(item.source.policy);
|
|
9911
|
+
for (const ref of topicReferences(policy)) {
|
|
9912
|
+
const topic = mappedTopics.get(topicKey(ref));
|
|
9913
|
+
if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
|
|
9914
|
+
ref.topic_id = topic.topic_id;
|
|
9915
|
+
ref.topic_name = topic.topic_name;
|
|
9916
|
+
ref.revision = topic.revision;
|
|
9917
|
+
}
|
|
9918
|
+
for (const ref of dlpReferences(policy)) {
|
|
9919
|
+
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
9920
|
+
if (!mapping) continue;
|
|
9921
|
+
if (ref.kind === "embedded") {
|
|
9922
|
+
ref.value.name = mapping.destination;
|
|
9923
|
+
ref.value.uuid = mapping.id;
|
|
9924
|
+
} else ref.value.text = mapping.destination;
|
|
9925
|
+
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
9926
|
+
if ("version" in ref.value) {
|
|
9927
|
+
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
9928
|
+
ref.value.version = String(mapping.version);
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
const body = CreateSecurityProfileRequestSchema.parse({
|
|
9932
|
+
profile_name: item.name,
|
|
9933
|
+
active: item.source.active,
|
|
9934
|
+
policy
|
|
9935
|
+
});
|
|
9936
|
+
const inventory2 = await profiles(api, plan.maxPages);
|
|
9937
|
+
assertTenant(inventory2, plan.destinationTsgId);
|
|
9938
|
+
const current = inventory2.find((p) => p.profile_name === item.name);
|
|
9939
|
+
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9940
|
+
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
9941
|
+
const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
|
|
9942
|
+
result.profiles.push({
|
|
9943
|
+
name: item.name,
|
|
9944
|
+
action: item.action === "create" ? "created" : "updated",
|
|
9945
|
+
id: response.profile_id
|
|
9946
|
+
});
|
|
9947
|
+
const verified = (await profiles(api, plan.maxPages)).find(
|
|
9948
|
+
(p) => p.profile_name === item.name
|
|
9949
|
+
);
|
|
9950
|
+
if (verified) assertTenant([verified], plan.destinationTsgId);
|
|
9951
|
+
if (!verified?.profile_id || response.profile_id && response.profile_id !== verified.profile_id || item.source.active !== void 0 && verified.active !== item.source.active || canonical(verified.policy) !== canonical(policy))
|
|
9952
|
+
throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
9953
|
+
}
|
|
9954
|
+
result.complete = true;
|
|
9955
|
+
} catch (error) {
|
|
9956
|
+
const status = error?.statusCode;
|
|
9957
|
+
result.error = Number.isInteger(status) ? `API request failed (HTTP ${status}); verify destination state before retrying` : error instanceof Error && /^(Destination|Topic response|Missing planned|Restored profile|Inventory|DLP inventory)/.test(
|
|
9958
|
+
error.message
|
|
9959
|
+
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
9960
|
+
}
|
|
9961
|
+
return result;
|
|
9962
|
+
}
|
|
9963
|
+
|
|
9964
|
+
// src/cli/commands/profile-transfer.ts
|
|
9965
|
+
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
9966
|
+
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
9967
|
+
function maxPages(value) {
|
|
9968
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
9969
|
+
usageError("--max-pages must be an integer from 1 to 1000");
|
|
9970
|
+
return Number(value);
|
|
9971
|
+
}
|
|
9972
|
+
function dlpMap(values) {
|
|
9973
|
+
const entries = values.map((value) => {
|
|
9974
|
+
const at = value.indexOf("=");
|
|
9975
|
+
if (at < 1 || at === value.length - 1)
|
|
9976
|
+
throw new CliUsageError("--dlp-map requires source-name=destination-name");
|
|
9977
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
9978
|
+
});
|
|
9979
|
+
if (new Set(entries.map(([key]) => key)).size !== entries.length)
|
|
9980
|
+
throw new CliUsageError("Duplicate source names in --dlp-map");
|
|
9981
|
+
return Object.fromEntries(entries);
|
|
9982
|
+
}
|
|
9983
|
+
async function context() {
|
|
9984
|
+
const config = await loadConfig();
|
|
9985
|
+
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
9986
|
+
throw new CliUsageError(
|
|
9987
|
+
"Profile transfer requires configured Management client ID, secret and TSG ID"
|
|
9988
|
+
);
|
|
9989
|
+
const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
|
|
9990
|
+
return {
|
|
9991
|
+
api: {
|
|
9992
|
+
profiles: client.profiles,
|
|
9993
|
+
topics: client.topics,
|
|
9994
|
+
dataProfiles: client.dlp.dataProfiles
|
|
9995
|
+
},
|
|
9996
|
+
tsgId: config.mgmtTsgId
|
|
9997
|
+
};
|
|
9998
|
+
}
|
|
9999
|
+
function failure(error) {
|
|
10000
|
+
if (error instanceof CliUsageError) usageError(error.message);
|
|
10001
|
+
if (error instanceof ZodError) {
|
|
10002
|
+
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
10003
|
+
} else if (error instanceof AISecSDKException2) {
|
|
10004
|
+
ui.error(
|
|
10005
|
+
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10006
|
+
);
|
|
10007
|
+
} else if (error?.code) {
|
|
10008
|
+
ui.error(
|
|
10009
|
+
`Profile transfer file operation failed (${error.code}); existing files are not overwritten.`
|
|
10010
|
+
);
|
|
10011
|
+
} else {
|
|
10012
|
+
ui.error(error instanceof Error ? error.message : "Profile transfer failed");
|
|
10013
|
+
}
|
|
10014
|
+
process.exitCode = 1;
|
|
10015
|
+
}
|
|
10016
|
+
function registerProfileTransferCommands(profiles2) {
|
|
10017
|
+
const backup = profiles2.command("backup [profile]").description(
|
|
10018
|
+
"Back up latest Runtime profile policies and referenced topic definitions; no argument means all profiles"
|
|
10019
|
+
).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(
|
|
10020
|
+
"--max-pages <n>",
|
|
10021
|
+
"Maximum pages per inventory; incomplete exports fail",
|
|
10022
|
+
maxPages,
|
|
10023
|
+
100
|
|
10024
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10025
|
+
"after",
|
|
10026
|
+
examples(
|
|
10027
|
+
"airs runtime profiles backup --all --output-file ./profiles.json",
|
|
10028
|
+
'airs runtime profiles backup "Production" --file-format yaml --output-file ./production.yaml'
|
|
10029
|
+
)
|
|
10030
|
+
).action(async (profile, opts) => {
|
|
10031
|
+
try {
|
|
10032
|
+
if (opts.all && profile)
|
|
10033
|
+
throw new CliUsageError("Choose either a profile selector or --all");
|
|
10034
|
+
if (!["json", "yaml"].includes(opts.fileFormat))
|
|
10035
|
+
throw new CliUsageError("--file-format must be json or yaml");
|
|
10036
|
+
const format = await resolveOutput(backup, opts);
|
|
10037
|
+
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10038
|
+
if (opts.outputFile === "-")
|
|
10039
|
+
throw new CliUsageError("Backups require a private file destination, not stdout");
|
|
10040
|
+
const path3 = resolve5(
|
|
10041
|
+
opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
|
|
10042
|
+
);
|
|
10043
|
+
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10044
|
+
extname(path3).toLowerCase()
|
|
10045
|
+
))
|
|
10046
|
+
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10047
|
+
const { api, tsgId } = await context();
|
|
10048
|
+
const data = await backupRuntimeProfiles(api, tsgId, { profile, maxPages: opts.maxPages });
|
|
10049
|
+
const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10050
|
+
`;
|
|
10051
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
|
|
10052
|
+
throw new Error("Backup exceeds the 20 MiB size limit; select individual profiles");
|
|
10053
|
+
await writeReportFile(path3, content);
|
|
10054
|
+
console.log(
|
|
10055
|
+
formatOutput(
|
|
10056
|
+
[
|
|
10057
|
+
{
|
|
10058
|
+
file: path3,
|
|
10059
|
+
sourceTsgId: tsgId,
|
|
10060
|
+
profiles: data.profiles.length,
|
|
10061
|
+
topics: data.topics.length
|
|
10062
|
+
}
|
|
10063
|
+
],
|
|
10064
|
+
[
|
|
10065
|
+
{ key: "file", label: "Backup file" },
|
|
10066
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10067
|
+
{ key: "profiles", label: "Profiles" },
|
|
10068
|
+
{ key: "topics", label: "Topics" }
|
|
10069
|
+
],
|
|
10070
|
+
format === "pretty" ? "table" : format
|
|
10071
|
+
)
|
|
10072
|
+
);
|
|
10073
|
+
} catch (error) {
|
|
10074
|
+
failure(error);
|
|
10075
|
+
}
|
|
10076
|
+
});
|
|
10077
|
+
const restore = profiles2.command("restore <file>").description(
|
|
10078
|
+
"Restore profile policies into the selected tenant; validate all dependencies before changes"
|
|
10079
|
+
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option("--on-conflict <policy>", "Existing profile names: error, skip or update", "error").option(
|
|
10080
|
+
"--dlp-map <source=destination>",
|
|
10081
|
+
"Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
|
|
10082
|
+
(value, prior) => [...prior, value],
|
|
10083
|
+
[]
|
|
10084
|
+
).option(
|
|
10085
|
+
"--max-pages <n>",
|
|
10086
|
+
"Maximum pages per inventory; incomplete inventories fail",
|
|
10087
|
+
maxPages,
|
|
10088
|
+
100
|
|
10089
|
+
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10090
|
+
"--force",
|
|
10091
|
+
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10092
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10093
|
+
"after",
|
|
10094
|
+
examples(
|
|
10095
|
+
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
10096
|
+
"airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10097
|
+
)
|
|
10098
|
+
).action(async (file, opts) => {
|
|
10099
|
+
try {
|
|
10100
|
+
if (!["error", "skip", "update"].includes(opts.onConflict))
|
|
10101
|
+
throw new CliUsageError("--on-conflict must be error, skip or update");
|
|
10102
|
+
if (opts.force && !opts.expectTsg)
|
|
10103
|
+
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10104
|
+
const mappings = dlpMap(opts.dlpMap);
|
|
10105
|
+
const path3 = resolve5(file);
|
|
10106
|
+
const extension = extname(path3).toLowerCase();
|
|
10107
|
+
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10108
|
+
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10109
|
+
const info = await stat(path3);
|
|
10110
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10111
|
+
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10112
|
+
let input;
|
|
10113
|
+
try {
|
|
10114
|
+
const text2 = await readFile9(path3, "utf8");
|
|
10115
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10116
|
+
input = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10117
|
+
} catch {
|
|
10118
|
+
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10119
|
+
}
|
|
10120
|
+
const data = RuntimeProfilesBackupSchema.parse(input);
|
|
10121
|
+
const format = await resolveOutput(restore, opts);
|
|
10122
|
+
const { api, tsgId } = await context();
|
|
10123
|
+
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10124
|
+
throw new CliUsageError(
|
|
10125
|
+
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
10126
|
+
);
|
|
10127
|
+
const plan = await planRuntimeProfilesRestore(api, data, tsgId, {
|
|
10128
|
+
namePrefix: opts.namePrefix,
|
|
10129
|
+
onConflict: opts.onConflict,
|
|
10130
|
+
maxPages: opts.maxPages,
|
|
10131
|
+
dlpMap: mappings
|
|
10132
|
+
});
|
|
10133
|
+
const summary = {
|
|
10134
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10135
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10136
|
+
dryRun: Boolean(opts.dryRun),
|
|
10137
|
+
profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action })),
|
|
10138
|
+
topics: plan.topics.map((t) => ({
|
|
10139
|
+
name: t.name,
|
|
10140
|
+
action: t.existing ? "reuse" : "create"
|
|
10141
|
+
})),
|
|
10142
|
+
dlpMappings: plan.dlpMappings.map((m) => ({
|
|
10143
|
+
source: m.source,
|
|
10144
|
+
destination: m.destination
|
|
10145
|
+
}))
|
|
10146
|
+
};
|
|
10147
|
+
const columns = [
|
|
10148
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10149
|
+
{ key: "destinationTsgId", label: "Destination TSG" },
|
|
10150
|
+
{ key: "profiles", label: "Profiles" },
|
|
10151
|
+
{ key: "topics", label: "Topics" },
|
|
10152
|
+
{ key: "dlpMappings", label: "DLP mappings" },
|
|
10153
|
+
{ key: "dryRun", label: "Dry run" },
|
|
10154
|
+
{ key: "complete", label: "Complete" }
|
|
10155
|
+
];
|
|
10156
|
+
if (opts.dryRun) {
|
|
10157
|
+
console.log(formatOutput([summary], columns, format === "pretty" ? "table" : format));
|
|
10158
|
+
return;
|
|
10159
|
+
}
|
|
10160
|
+
const changing = plan.profiles.filter((p) => p.action !== "skip");
|
|
10161
|
+
if (changing.length)
|
|
10162
|
+
await confirmOrAbort(
|
|
10163
|
+
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates)?`,
|
|
10164
|
+
Boolean(opts.force),
|
|
10165
|
+
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10166
|
+
);
|
|
10167
|
+
const result = await restoreRuntimeProfiles(api, plan);
|
|
10168
|
+
console.log(formatOutput([{ ...result }], columns, format === "pretty" ? "table" : format));
|
|
10169
|
+
if (!result.complete) {
|
|
10170
|
+
ui.error(result.error ?? "Restore incomplete");
|
|
10171
|
+
process.exitCode = 1;
|
|
10172
|
+
}
|
|
10173
|
+
} catch (error) {
|
|
10174
|
+
failure(error);
|
|
10175
|
+
}
|
|
10176
|
+
});
|
|
10177
|
+
}
|
|
10178
|
+
|
|
9528
10179
|
// src/cli/commands/profiles-cleanup.ts
|
|
9529
10180
|
import { execSync } from "child_process";
|
|
9530
|
-
function findDuplicateProfiles(
|
|
10181
|
+
function findDuplicateProfiles(profiles2) {
|
|
9531
10182
|
const groups = /* @__PURE__ */ new Map();
|
|
9532
|
-
for (const p of
|
|
10183
|
+
for (const p of profiles2) {
|
|
9533
10184
|
const rev = p.revision ?? 0;
|
|
9534
10185
|
const entry = { id: p.profileId, revision: rev };
|
|
9535
10186
|
const existing = groups.get(p.profileName);
|
|
@@ -9628,8 +10279,8 @@ function registerCleanupCommand(parent) {
|
|
|
9628
10279
|
|
|
9629
10280
|
// src/cli/commands/runtime-dashboard.ts
|
|
9630
10281
|
import { lstat as lstat4 } from "fs/promises";
|
|
9631
|
-
import { resolve as
|
|
9632
|
-
import { dump as
|
|
10282
|
+
import { resolve as resolve6 } from "path";
|
|
10283
|
+
import { dump as dump7 } from "js-yaml";
|
|
9633
10284
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
9634
10285
|
function integer3(value, name, minimum = 0) {
|
|
9635
10286
|
const n = Number(value);
|
|
@@ -9704,7 +10355,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9704
10355
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9705
10356
|
);
|
|
9706
10357
|
if (opts.outputFile) {
|
|
9707
|
-
const path3 =
|
|
10358
|
+
const path3 = resolve6(text(opts.outputFile, "--output-file"));
|
|
9708
10359
|
const existing = await lstat4(path3).catch((error) => {
|
|
9709
10360
|
if (error.code !== "ENOENT") throw error;
|
|
9710
10361
|
return void 0;
|
|
@@ -9718,9 +10369,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9718
10369
|
numRetries: 0
|
|
9719
10370
|
});
|
|
9720
10371
|
const result = await read(client.dashboard);
|
|
9721
|
-
const output = format === "yaml" ?
|
|
10372
|
+
const output = format === "yaml" ? dump7(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9722
10373
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9723
|
-
await writeReportFile(
|
|
10374
|
+
await writeReportFile(resolve6(String(opts.outputFile)), `${output}
|
|
9724
10375
|
`);
|
|
9725
10376
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9726
10377
|
} else console.log(output);
|
|
@@ -9892,8 +10543,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9892
10543
|
|
|
9893
10544
|
// src/cli/commands/runtime-report.ts
|
|
9894
10545
|
import { lstat as lstat5 } from "fs/promises";
|
|
9895
|
-
import { resolve as
|
|
9896
|
-
import { randomUUID as
|
|
10546
|
+
import { resolve as resolve7 } from "path";
|
|
10547
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
9897
10548
|
function registerRuntimeReportCommand(runtime) {
|
|
9898
10549
|
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(
|
|
9899
10550
|
"--output-file <path>",
|
|
@@ -9910,15 +10561,15 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9910
10561
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
9911
10562
|
if (format !== "html" && format !== "markdown")
|
|
9912
10563
|
usageError("Report output must be html or markdown");
|
|
9913
|
-
const
|
|
9914
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
10564
|
+
const maxPages2 = Number(opts.maxPages);
|
|
10565
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
9915
10566
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
9916
10567
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
9917
10568
|
usageError("--title must contain 1\u2013240 characters");
|
|
9918
10569
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9919
10570
|
usageError("--output-file cannot be empty");
|
|
9920
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9921
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
10571
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID8().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
10572
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve7(opts.outputFile ?? defaultName);
|
|
9922
10573
|
try {
|
|
9923
10574
|
if (outputPath) {
|
|
9924
10575
|
const existing = await lstat5(outputPath).catch((error) => {
|
|
@@ -9933,7 +10584,7 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9933
10584
|
numRetries: 0
|
|
9934
10585
|
});
|
|
9935
10586
|
ui.status("Collecting daily runtime activity and current configuration (read-only)...");
|
|
9936
|
-
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages });
|
|
10587
|
+
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages2 });
|
|
9937
10588
|
const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
|
|
9938
10589
|
if (outputPath) {
|
|
9939
10590
|
await writeReportFile(outputPath, rendered);
|
|
@@ -10093,7 +10744,7 @@ function registerCreateCommand(parent) {
|
|
|
10093
10744
|
}
|
|
10094
10745
|
|
|
10095
10746
|
// src/cli/commands/topics-eval.ts
|
|
10096
|
-
import { readFile as
|
|
10747
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
10097
10748
|
|
|
10098
10749
|
// src/core/prompt-loader.ts
|
|
10099
10750
|
function parseCsvLine(line) {
|
|
@@ -10236,7 +10887,7 @@ function registerEvalCommand(parent) {
|
|
|
10236
10887
|
resolveDeprecatedAliases(cmd, opts);
|
|
10237
10888
|
try {
|
|
10238
10889
|
const config = await loadConfig();
|
|
10239
|
-
const csvContent = await
|
|
10890
|
+
const csvContent = await readFile10(opts.prompts, "utf-8");
|
|
10240
10891
|
const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
|
|
10241
10892
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
10242
10893
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
@@ -10260,8 +10911,8 @@ function registerEvalCommand(parent) {
|
|
|
10260
10911
|
|
|
10261
10912
|
// src/cli/commands/topics-revert.ts
|
|
10262
10913
|
async function revertTopic(mgmt, profileName, topicName) {
|
|
10263
|
-
const
|
|
10264
|
-
const match =
|
|
10914
|
+
const topics2 = await mgmt.listTopics();
|
|
10915
|
+
const match = topics2.find((t) => t.topic_name === topicName);
|
|
10265
10916
|
if (!match || !match.topic_id) {
|
|
10266
10917
|
throw new Error(`Topic "${topicName}" not found`);
|
|
10267
10918
|
}
|
|
@@ -10408,7 +11059,7 @@ function completedBulkResults(state) {
|
|
|
10408
11059
|
}
|
|
10409
11060
|
async function writeBulkResults(outputPath, results) {
|
|
10410
11061
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
10411
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
11062
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID9()}`;
|
|
10412
11063
|
try {
|
|
10413
11064
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
10414
11065
|
encoding: "utf-8",
|
|
@@ -10535,7 +11186,7 @@ function registerRuntimeCommand(program) {
|
|
|
10535
11186
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
10536
11187
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
10537
11188
|
}
|
|
10538
|
-
const raw = await
|
|
11189
|
+
const raw = await readFile11(opts.file, "utf-8");
|
|
10539
11190
|
const prompts = parseInputFile(raw, opts.file);
|
|
10540
11191
|
if (prompts.length === 0) {
|
|
10541
11192
|
usageError("No prompts found in input file");
|
|
@@ -10750,16 +11401,17 @@ function registerRuntimeCommand(program) {
|
|
|
10750
11401
|
const fmt = await resolveOutput(deploymentProfilesList, opts);
|
|
10751
11402
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
10752
11403
|
const service = await createMgmtService();
|
|
10753
|
-
const
|
|
11404
|
+
const profiles3 = await service.listDeploymentProfiles({
|
|
10754
11405
|
unactivated: opts.unactivated
|
|
10755
11406
|
});
|
|
10756
|
-
renderDeploymentProfileList(
|
|
11407
|
+
renderDeploymentProfileList(profiles3, fmt);
|
|
10757
11408
|
} catch (err) {
|
|
10758
11409
|
fail(err);
|
|
10759
11410
|
}
|
|
10760
11411
|
});
|
|
10761
|
-
const
|
|
10762
|
-
|
|
11412
|
+
const profiles2 = runtime.command("profiles").description("Manage AIRS security profiles");
|
|
11413
|
+
registerProfileTransferCommands(profiles2);
|
|
11414
|
+
const profilesList = registerListFlags(profiles2.command("list"), { dialect: "offset" }).description("List security profiles").option("--all-versions", "Include every profile revision").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
|
|
10763
11415
|
"after",
|
|
10764
11416
|
examples(
|
|
10765
11417
|
"airs runtime profiles list",
|
|
@@ -10798,7 +11450,7 @@ function registerRuntimeCommand(program) {
|
|
|
10798
11450
|
fail(err);
|
|
10799
11451
|
}
|
|
10800
11452
|
});
|
|
10801
|
-
const profilesGet =
|
|
11453
|
+
const profilesGet = profiles2.command("get <nameOrId>").description("Get a security profile by name or UUID").option("--revision <n>", "Select an exact revision", Number).option("--all-versions", "Return every matching revision").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (nameOrId, opts) => {
|
|
10802
11454
|
try {
|
|
10803
11455
|
const fmt = await resolveOutput(profilesGet, opts);
|
|
10804
11456
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
@@ -10807,10 +11459,10 @@ function registerRuntimeCommand(program) {
|
|
|
10807
11459
|
nameOrId
|
|
10808
11460
|
);
|
|
10809
11461
|
if (opts.revision !== void 0 || opts.allVersions) {
|
|
10810
|
-
const
|
|
11462
|
+
const profiles3 = (await service.listAllProfiles({ latest: false })).filter(
|
|
10811
11463
|
(profile) => isUuid ? profile.profileId === nameOrId : profile.profileName === nameOrId
|
|
10812
11464
|
);
|
|
10813
|
-
const selected = opts.revision === void 0 ?
|
|
11465
|
+
const selected = opts.revision === void 0 ? profiles3 : profiles3.filter((profile) => profile.revision === opts.revision);
|
|
10814
11466
|
if (selected.length === 0) throw new Error(`Profile ${nameOrId} not found`);
|
|
10815
11467
|
if (opts.allVersions) emitList(profilesView, selected, fmt);
|
|
10816
11468
|
else emitDetail(profilesView, selected[0], fmt);
|
|
@@ -10822,7 +11474,7 @@ function registerRuntimeCommand(program) {
|
|
|
10822
11474
|
fail(err);
|
|
10823
11475
|
}
|
|
10824
11476
|
});
|
|
10825
|
-
|
|
11477
|
+
profiles2.command("create").description("Create a new security profile").requiredOption("--name <name>", "Profile name").option("--no-active", "Create profile as inactive").option("--prompt-injection <action>", "Prompt injection action (block/allow/alert)").option("--toxic-content <action>", 'Toxic content action (e.g. "high:block, moderate:block")').option("--contextual-grounding <action>", "Contextual grounding action (block/allow/alert)").option("--malicious-code <action>", "Malicious code protection action (block/allow/alert)").option("--url-action <action>", "URL detected action (block/allow/alert)").option("--allow-url-categories <list>", "Comma-separated URL categories to allow").option("--block-url-categories <list>", "Comma-separated URL categories to block").option("--alert-url-categories <list>", "Comma-separated URL categories to alert").option("--agent-security <action>", "Agent security action (block/allow/alert)").option("--dlp-action <action>", "Data leak detection action (block/allow/alert)").option("--dlp-profiles <list>", "Comma-separated DLP profile names").option("--mask-data-inline", "Mask detected data inline").option("--db-security-create <action>", "Database create action (block/allow/alert)").option("--db-security-read <action>", "Database read action (block/allow/alert)").option("--db-security-update <action>", "Database update action (block/allow/alert)").option("--db-security-delete <action>", "Database delete action (block/allow/alert)").option("--inline-timeout-action <action>", "Inline timeout action (block/allow)").option("--max-inline-latency <n>", "Max inline latency in seconds", Number.parseFloat).option("--mask-data-in-storage", "Mask data in storage").option("--config <path>", "JSON file with profile configuration (legacy)").action(async (opts) => {
|
|
10826
11478
|
const service = await createMgmtService();
|
|
10827
11479
|
try {
|
|
10828
11480
|
renderRuntimeConfigHeader();
|
|
@@ -10877,7 +11529,7 @@ function registerRuntimeCommand(program) {
|
|
|
10877
11529
|
fail(err);
|
|
10878
11530
|
}
|
|
10879
11531
|
});
|
|
10880
|
-
|
|
11532
|
+
profiles2.command("update <nameOrId>").description("Update a security profile by name or UUID").option("--name <name>", "Update profile name").option("--no-active", "Set profile as inactive").option("--active", "Set profile as active").option("--prompt-injection <action>", "Prompt injection action (block/allow/alert)").option("--toxic-content <action>", 'Toxic content action (e.g. "high:block, moderate:block")').option("--contextual-grounding <action>", "Contextual grounding action (block/allow/alert)").option("--malicious-code <action>", "Malicious code protection action (block/allow/alert)").option("--url-action <action>", "URL detected action (block/allow/alert)").option("--allow-url-categories <list>", "Comma-separated URL categories to allow").option("--block-url-categories <list>", "Comma-separated URL categories to block").option("--alert-url-categories <list>", "Comma-separated URL categories to alert").option("--agent-security <action>", "Agent security action (block/allow/alert)").option("--dlp-action <action>", "Data leak detection action (block/allow/alert)").option("--dlp-profiles <list>", "Comma-separated DLP profile names").option("--mask-data-inline", "Mask detected data inline").option("--db-security-create <action>", "Database create action (block/allow/alert)").option("--db-security-read <action>", "Database read action (block/allow/alert)").option("--db-security-update <action>", "Database update action (block/allow/alert)").option("--db-security-delete <action>", "Database delete action (block/allow/alert)").option("--inline-timeout-action <action>", "Inline timeout action (block/allow)").option("--max-inline-latency <n>", "Max inline latency in seconds", Number.parseFloat).option("--mask-data-in-storage", "Mask data in storage").option("--config <path>", "JSON file with profile updates (legacy)").action(async (nameOrId, opts) => {
|
|
10881
11533
|
try {
|
|
10882
11534
|
renderRuntimeConfigHeader();
|
|
10883
11535
|
const service = await createMgmtService();
|
|
@@ -10926,7 +11578,7 @@ function registerRuntimeCommand(program) {
|
|
|
10926
11578
|
fail(err);
|
|
10927
11579
|
}
|
|
10928
11580
|
});
|
|
10929
|
-
|
|
11581
|
+
profiles2.command("delete <nameOrId>").description("Delete a security profile by name or UUID").option("--force", "Skip confirmation and force delete (removes from referencing policies)").option("--updated-by <email>", "Email of user performing force deletion").action(async (nameOrId, opts) => {
|
|
10930
11582
|
try {
|
|
10931
11583
|
renderRuntimeConfigHeader();
|
|
10932
11584
|
const service = await createMgmtService();
|
|
@@ -10959,7 +11611,7 @@ function registerRuntimeCommand(program) {
|
|
|
10959
11611
|
fail(err);
|
|
10960
11612
|
}
|
|
10961
11613
|
});
|
|
10962
|
-
registerCleanupCommand(
|
|
11614
|
+
registerCleanupCommand(profiles2);
|
|
10963
11615
|
const resumePoll = runtime.command("resume-poll <stateFile>").description("Resume polling for a previously submitted bulk scan").option("--output-file <file>", "Output CSV file path");
|
|
10964
11616
|
registerDeprecatedAlias(resumePoll, {
|
|
10965
11617
|
oldFlag: "--output <file>",
|
|
@@ -11107,10 +11759,10 @@ function registerRuntimeCommand(program) {
|
|
|
11107
11759
|
);
|
|
11108
11760
|
process.exitCode = 1;
|
|
11109
11761
|
});
|
|
11110
|
-
const
|
|
11111
|
-
registerApplyCommand(
|
|
11112
|
-
registerCreateCommand(
|
|
11113
|
-
|
|
11762
|
+
const topics2 = runtime.command("topics").description("Manage AIRS custom topics and guardrail generation");
|
|
11763
|
+
registerApplyCommand(topics2);
|
|
11764
|
+
registerCreateCommand(topics2);
|
|
11765
|
+
topics2.command("delete <topicId>").description("Delete a custom topic").option("--force", "Skip confirmation and force delete (removes from all referencing profiles)").option("--updated-by <email>", "Email of user performing force deletion").action(async (topicId, opts) => {
|
|
11114
11766
|
try {
|
|
11115
11767
|
renderRuntimeConfigHeader();
|
|
11116
11768
|
const service = await createMgmtService();
|
|
@@ -11128,8 +11780,8 @@ function registerRuntimeCommand(program) {
|
|
|
11128
11780
|
fail(err);
|
|
11129
11781
|
}
|
|
11130
11782
|
});
|
|
11131
|
-
registerEvalCommand(
|
|
11132
|
-
const topicsGet =
|
|
11783
|
+
registerEvalCommand(topics2);
|
|
11784
|
+
const topicsGet = topics2.command("get <nameOrId>").description("Get a custom topic by name or UUID").option("--revision <n>", "Select an exact revision", Number).option("--all-versions", "Return every matching revision").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (nameOrId, opts) => {
|
|
11133
11785
|
try {
|
|
11134
11786
|
const fmt = await resolveOutput(topicsGet, opts);
|
|
11135
11787
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
@@ -11138,10 +11790,10 @@ function registerRuntimeCommand(program) {
|
|
|
11138
11790
|
nameOrId
|
|
11139
11791
|
);
|
|
11140
11792
|
if (opts.revision !== void 0 || opts.allVersions) {
|
|
11141
|
-
const
|
|
11793
|
+
const topics3 = (await service.listTopics()).filter(
|
|
11142
11794
|
(topic) => isUuid ? topic.topic_id === nameOrId : topic.topic_name === nameOrId
|
|
11143
11795
|
);
|
|
11144
|
-
const selected = opts.revision === void 0 ?
|
|
11796
|
+
const selected = opts.revision === void 0 ? topics3 : topics3.filter((topic) => topic.revision === opts.revision);
|
|
11145
11797
|
if (selected.length === 0) throw new Error(`Topic ${nameOrId} not found`);
|
|
11146
11798
|
if (opts.allVersions) emitList(topicsView, selected, fmt);
|
|
11147
11799
|
else emitDetail(topicsView, selected[0], fmt);
|
|
@@ -11153,7 +11805,7 @@ function registerRuntimeCommand(program) {
|
|
|
11153
11805
|
fail(err);
|
|
11154
11806
|
}
|
|
11155
11807
|
});
|
|
11156
|
-
const topicsList = registerListFlags(
|
|
11808
|
+
const topicsList = registerListFlags(topics2.command("list"), { dialect: "offset" }).description("List custom topics").option("--all-versions", "Include every topic revision").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (opts) => {
|
|
11157
11809
|
try {
|
|
11158
11810
|
const fmt = await resolveOutput(topicsList, opts);
|
|
11159
11811
|
const params = resolveListParams(topicsList, opts, { dialect: "offset" });
|
|
@@ -11174,9 +11826,9 @@ function registerRuntimeCommand(program) {
|
|
|
11174
11826
|
fail(err);
|
|
11175
11827
|
}
|
|
11176
11828
|
});
|
|
11177
|
-
registerRevertCommand(
|
|
11178
|
-
registerSampleCommand(
|
|
11179
|
-
|
|
11829
|
+
registerRevertCommand(topics2);
|
|
11830
|
+
registerSampleCommand(topics2);
|
|
11831
|
+
topics2.command("update <topicId>").description("Update a custom topic").requiredOption("--config <path>", "JSON file with topic updates").action(async (topicId, opts) => {
|
|
11180
11832
|
try {
|
|
11181
11833
|
renderRuntimeConfigHeader();
|
|
11182
11834
|
const service = await createMgmtService();
|
|
@@ -11191,6 +11843,114 @@ function registerRuntimeCommand(program) {
|
|
|
11191
11843
|
registerDlpCommands(runtime);
|
|
11192
11844
|
}
|
|
11193
11845
|
|
|
11846
|
+
// src/cli/commands/tenant.ts
|
|
11847
|
+
var COLUMNS2 = [
|
|
11848
|
+
{ key: "name", label: "Tenant" },
|
|
11849
|
+
{ key: "active", label: "Selected" },
|
|
11850
|
+
{ key: "tsgId", label: "TSG ID" },
|
|
11851
|
+
{ key: "configPath", label: "Config file" }
|
|
11852
|
+
];
|
|
11853
|
+
function registerTenantCommand(program) {
|
|
11854
|
+
const tenant = program.command("tenant").description("Select named tenant config files without changing their contents").addHelpText(
|
|
11855
|
+
"after",
|
|
11856
|
+
examples(
|
|
11857
|
+
"airs tenant create production --config /secure/production.json",
|
|
11858
|
+
"airs tenant switch production",
|
|
11859
|
+
"airs tenant read",
|
|
11860
|
+
"airs tenant switch default"
|
|
11861
|
+
)
|
|
11862
|
+
);
|
|
11863
|
+
tenant.command("create <name>").description("Register an existing JSON config file by reference; does not activate it").requiredOption("--config <path>", "Existing Prisma AIRS JSON config file").action(async (name, opts) => {
|
|
11864
|
+
try {
|
|
11865
|
+
const entry = await createTenant(name, opts.config);
|
|
11866
|
+
ui.success(
|
|
11867
|
+
`Registered ${entry.name} (TSG ${entry.tsgId}); config file unchanged. Use airs tenant switch ${entry.name}.`
|
|
11868
|
+
);
|
|
11869
|
+
} catch (error) {
|
|
11870
|
+
fail(error);
|
|
11871
|
+
}
|
|
11872
|
+
});
|
|
11873
|
+
tenant.command("switch <name>").description(
|
|
11874
|
+
"Persist the selected tenant; default restores legacy config/environment resolution"
|
|
11875
|
+
).action(async (name) => {
|
|
11876
|
+
try {
|
|
11877
|
+
const entry = await switchTenant(name);
|
|
11878
|
+
ui.success(
|
|
11879
|
+
entry ? `Selected ${entry.name} (TSG ${entry.tsgId})` : "Selected default; explicit config/environment overrides still apply"
|
|
11880
|
+
);
|
|
11881
|
+
} catch (error) {
|
|
11882
|
+
fail(error);
|
|
11883
|
+
}
|
|
11884
|
+
});
|
|
11885
|
+
const list = tenant.command("list").description("List registrations without reading credentials").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (opts) => {
|
|
11886
|
+
try {
|
|
11887
|
+
const format = await resolveOutput(list, opts, { ignoreConfig: true });
|
|
11888
|
+
const store = readTenantStore();
|
|
11889
|
+
const explicit = process.env.PRISMA_AIRS_CONFIG_PATH;
|
|
11890
|
+
const rows = [
|
|
11891
|
+
{
|
|
11892
|
+
name: "default",
|
|
11893
|
+
active: store.active === null,
|
|
11894
|
+
tsgId: "",
|
|
11895
|
+
configPath: defaultTenantConfigPath()
|
|
11896
|
+
},
|
|
11897
|
+
...store.tenants.map((entry) => ({ ...entry, active: entry.name === store.active }))
|
|
11898
|
+
];
|
|
11899
|
+
if (explicit)
|
|
11900
|
+
ui.status("PRISMA_AIRS_CONFIG_PATH overrides the selected tenant for API commands.");
|
|
11901
|
+
console.log(formatOutput(rows, COLUMNS2, format === "pretty" ? "table" : format));
|
|
11902
|
+
} catch (error) {
|
|
11903
|
+
fail(error);
|
|
11904
|
+
}
|
|
11905
|
+
});
|
|
11906
|
+
const read = tenant.command("read [name]").description("Read a tenant config with all credential values redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
|
|
11907
|
+
try {
|
|
11908
|
+
const format = await resolveOutput(read, opts, { ignoreConfig: true });
|
|
11909
|
+
const store = readTenantStore();
|
|
11910
|
+
const selected = name ?? store.active ?? "default";
|
|
11911
|
+
const entry = store.tenants.find((value) => value.name === selected);
|
|
11912
|
+
if (selected !== "default" && !entry) throw new Error("Tenant not found");
|
|
11913
|
+
const path3 = entry?.configPath ?? expandConfigPath(process.env.PRISMA_AIRS_CONFIG_PATH || defaultTenantConfigPath());
|
|
11914
|
+
const config = readTenantConfig(path3, entry?.tsgId);
|
|
11915
|
+
const rows = Object.entries(config).map(([key, value]) => ({
|
|
11916
|
+
key,
|
|
11917
|
+
value: /key|secret|token|password/i.test(key) && value ? "[REDACTED]" : value ?? ""
|
|
11918
|
+
}));
|
|
11919
|
+
ui.status(`Tenant ${selected}${entry ? ` (TSG ${entry.tsgId})` : ""}: ${path3}`);
|
|
11920
|
+
console.log(
|
|
11921
|
+
formatOutput(
|
|
11922
|
+
rows,
|
|
11923
|
+
[
|
|
11924
|
+
{ key: "key", label: "Key" },
|
|
11925
|
+
{ key: "value", label: "Value" }
|
|
11926
|
+
],
|
|
11927
|
+
format === "pretty" ? "table" : format
|
|
11928
|
+
)
|
|
11929
|
+
);
|
|
11930
|
+
} catch (error) {
|
|
11931
|
+
fail(error);
|
|
11932
|
+
}
|
|
11933
|
+
});
|
|
11934
|
+
tenant.command("delete <name>").description("Unregister an inactive tenant; retain its source config file").option("--force", "Skip interactive confirmation; active tenant deletion remains refused").action(async (name, opts) => {
|
|
11935
|
+
try {
|
|
11936
|
+
const store = readTenantStore();
|
|
11937
|
+
if (name === "default" || name === store.active)
|
|
11938
|
+
throw new Error("Switch away first; default and active tenants cannot be deleted");
|
|
11939
|
+
if (!store.tenants.some((entry) => entry.name === name))
|
|
11940
|
+
throw new Error("Tenant not found");
|
|
11941
|
+
await confirmOrAbort(
|
|
11942
|
+
`Unregister tenant ${name}? Its config file will be retained.`,
|
|
11943
|
+
Boolean(opts.force),
|
|
11944
|
+
{ action: `unregister tenant ${name}` }
|
|
11945
|
+
);
|
|
11946
|
+
await deleteTenant(name);
|
|
11947
|
+
ui.success(`Unregistered ${name}; source config file retained`);
|
|
11948
|
+
} catch (error) {
|
|
11949
|
+
fail(error);
|
|
11950
|
+
}
|
|
11951
|
+
});
|
|
11952
|
+
}
|
|
11953
|
+
|
|
11194
11954
|
// src/cli/program.ts
|
|
11195
11955
|
var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
11196
11956
|
"categories",
|
|
@@ -11254,6 +12014,11 @@ function buildProgram() {
|
|
|
11254
12014
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
11255
12015
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
11256
12016
|
setQuiet(Boolean(root.quiet));
|
|
12017
|
+
const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
|
|
12018
|
+
if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
12019
|
+
usageError(
|
|
12020
|
+
"Disable --debug and PANW_AI_SEC_DEBUG for profile transfer; backups can contain sensitive policy configuration"
|
|
12021
|
+
);
|
|
11257
12022
|
const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway", "agentguard"].includes(
|
|
11258
12023
|
actionCommand.parent?.name() ?? ""
|
|
11259
12024
|
) && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
|
|
@@ -11261,7 +12026,7 @@ function buildProgram() {
|
|
|
11261
12026
|
usageError(
|
|
11262
12027
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
11263
12028
|
);
|
|
11264
|
-
if (!isEnvironmentReport && !(actionCommand.name() === "report" && actionCommand.parent?.name() === "redteam") && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
12029
|
+
if (!isEnvironmentReport && actionCommand.parent?.name() !== "tenant" && !(actionCommand.name() === "report" && actionCommand.parent?.name() === "redteam") && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
11265
12030
|
try {
|
|
11266
12031
|
const format = await resolveOutput(actionCommand, actionCommand.opts());
|
|
11267
12032
|
actionCommand.setOptionValueWithSource("output", format, "implied");
|
|
@@ -11272,7 +12037,7 @@ function buildProgram() {
|
|
|
11272
12037
|
if (root.debug) {
|
|
11273
12038
|
const logPath = join4(
|
|
11274
12039
|
process.cwd(),
|
|
11275
|
-
`debug-api-${Date.now()}-${
|
|
12040
|
+
`debug-api-${Date.now()}-${randomUUID10().slice(0, 8)}.jsonl`
|
|
11276
12041
|
);
|
|
11277
12042
|
try {
|
|
11278
12043
|
let ancestor = actionCommand;
|
|
@@ -11297,6 +12062,7 @@ function buildProgram() {
|
|
|
11297
12062
|
registerAgentGuardCommand(program);
|
|
11298
12063
|
registerAiGatewayCommand(program);
|
|
11299
12064
|
registerConfigCommand(program);
|
|
12065
|
+
registerTenantCommand(program);
|
|
11300
12066
|
registerDoctorCommand(program);
|
|
11301
12067
|
registerCompletionCommand(program);
|
|
11302
12068
|
applyListDeleteAliases(program);
|