@cdot65/prisma-airs-cli 5.4.1 → 5.6.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-XQOICLSH.js} +220 -25
- package/dist/cli/index.js +1063 -127
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -13,11 +13,18 @@ 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
|
+
readTenantConfigFile,
|
|
27
|
+
readTenantStore,
|
|
21
28
|
renderEnvironmentReportHtml,
|
|
22
29
|
renderEnvironmentReportMarkdown,
|
|
23
30
|
renderRedTeamReportHtml,
|
|
@@ -27,10 +34,13 @@ import {
|
|
|
27
34
|
resolveConfigFilePath,
|
|
28
35
|
resolveOutputDir,
|
|
29
36
|
sanitizeFilename,
|
|
37
|
+
switchTenant,
|
|
38
|
+
tenantStorePath,
|
|
39
|
+
validateTenantName,
|
|
30
40
|
validateTopic,
|
|
31
41
|
writeBackupFile,
|
|
32
42
|
writeReportFile
|
|
33
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-XQOICLSH.js";
|
|
34
44
|
|
|
35
45
|
// src/cli/index.ts
|
|
36
46
|
import "dotenv/config";
|
|
@@ -49,9 +59,9 @@ function installProcessGuards() {
|
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
// src/cli/program.ts
|
|
52
|
-
import { randomUUID as
|
|
62
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
53
63
|
import { readFileSync as readFileSync4 } from "fs";
|
|
54
|
-
import { dirname as
|
|
64
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
55
65
|
import { fileURLToPath } from "url";
|
|
56
66
|
import { Command } from "commander";
|
|
57
67
|
|
|
@@ -132,8 +142,8 @@ function modelSecurityClientOptions(config) {
|
|
|
132
142
|
|
|
133
143
|
// src/reports/agentguard.ts
|
|
134
144
|
async function collectAgentGuardReport(client, options = {}) {
|
|
135
|
-
const
|
|
136
|
-
if (!Number.isSafeInteger(
|
|
145
|
+
const maxPages2 = options.maxPages ?? 40;
|
|
146
|
+
if (!Number.isSafeInteger(maxPages2) || maxPages2 < 1 || maxPages2 > 100)
|
|
137
147
|
throw new Error("maxPages must be between 1 and 100");
|
|
138
148
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
139
149
|
const end = now();
|
|
@@ -195,7 +205,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
195
205
|
const seen = /* @__PURE__ */ new Set();
|
|
196
206
|
let total;
|
|
197
207
|
try {
|
|
198
|
-
for (let page = 0; page <
|
|
208
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
199
209
|
const r = await read(rows.length);
|
|
200
210
|
s.pages++;
|
|
201
211
|
if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
|
|
@@ -212,7 +222,7 @@ async function collectAgentGuardReport(client, options = {}) {
|
|
|
212
222
|
if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
|
|
213
223
|
if (!r.rows.length) throw new Error();
|
|
214
224
|
}
|
|
215
|
-
s.notes.push(`Reached the ${
|
|
225
|
+
s.notes.push(`Reached the ${maxPages2}-page budget.`);
|
|
216
226
|
throw new Error();
|
|
217
227
|
} catch (error) {
|
|
218
228
|
if (!rows.length) throw error;
|
|
@@ -375,7 +385,7 @@ async function resolveOutput(command, opts, resolution = {}) {
|
|
|
375
385
|
const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
|
|
376
386
|
let configured;
|
|
377
387
|
try {
|
|
378
|
-
configured = (await loadConfig()).defaultOutput;
|
|
388
|
+
configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
|
|
379
389
|
} catch (error) {
|
|
380
390
|
if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
|
|
381
391
|
else throw error;
|
|
@@ -2594,13 +2604,13 @@ function statusColor4(status) {
|
|
|
2594
2604
|
function renderRuntimeConfigHeader() {
|
|
2595
2605
|
ui.header("Prisma AIRS \u2014 Runtime Configuration", "Security profile and topic management");
|
|
2596
2606
|
}
|
|
2597
|
-
function renderProfileList(
|
|
2598
|
-
if (
|
|
2607
|
+
function renderProfileList(profiles2, format = "pretty") {
|
|
2608
|
+
if (profiles2.length === 0) {
|
|
2599
2609
|
ui.emptyList("profiles");
|
|
2600
2610
|
return;
|
|
2601
2611
|
}
|
|
2602
2612
|
if (format !== "pretty") {
|
|
2603
|
-
const rows =
|
|
2613
|
+
const rows = profiles2.map((p) => ({
|
|
2604
2614
|
id: p.profileId,
|
|
2605
2615
|
name: p.profileName,
|
|
2606
2616
|
status: p.active ? "active" : "inactive",
|
|
@@ -2616,7 +2626,7 @@ function renderProfileList(profiles, format = "pretty") {
|
|
|
2616
2626
|
return;
|
|
2617
2627
|
}
|
|
2618
2628
|
ui.section("Security Profiles:");
|
|
2619
|
-
for (const p of
|
|
2629
|
+
for (const p of profiles2) {
|
|
2620
2630
|
ui.dim(p.profileId);
|
|
2621
2631
|
const status = p.active ? chalk9.green("active") : chalk9.yellow("inactive");
|
|
2622
2632
|
const rev = p.revision != null ? chalk9.dim(` rev:${p.revision}`) : "";
|
|
@@ -2704,13 +2714,13 @@ function renderCleanupResult(results, format = "pretty") {
|
|
|
2704
2714
|
}
|
|
2705
2715
|
console.log();
|
|
2706
2716
|
}
|
|
2707
|
-
function renderTopicList(
|
|
2708
|
-
if (
|
|
2717
|
+
function renderTopicList(topics2, format = "pretty") {
|
|
2718
|
+
if (topics2.length === 0) {
|
|
2709
2719
|
ui.emptyList("topics");
|
|
2710
2720
|
return;
|
|
2711
2721
|
}
|
|
2712
2722
|
if (format !== "pretty") {
|
|
2713
|
-
const rows =
|
|
2723
|
+
const rows = topics2.map((t) => ({
|
|
2714
2724
|
id: t.topic_id ?? "",
|
|
2715
2725
|
name: t.topic_name,
|
|
2716
2726
|
revision: t.revision ?? "",
|
|
@@ -2731,7 +2741,7 @@ function renderTopicList(topics, format = "pretty") {
|
|
|
2731
2741
|
return;
|
|
2732
2742
|
}
|
|
2733
2743
|
ui.section("Custom Topics:");
|
|
2734
|
-
for (const t of
|
|
2744
|
+
for (const t of topics2) {
|
|
2735
2745
|
ui.dim(String(t.topic_id));
|
|
2736
2746
|
const rev = t.revision != null ? chalk9.dim(` rev:${t.revision}`) : "";
|
|
2737
2747
|
const desc = t.description ? chalk9.dim(` \u2014 ${t.description.slice(0, 80)}`) : "";
|
|
@@ -2933,13 +2943,13 @@ function renderCustomerAppConsumption(data, format = "pretty") {
|
|
|
2933
2943
|
}
|
|
2934
2944
|
console.log();
|
|
2935
2945
|
}
|
|
2936
|
-
function renderDeploymentProfileList(
|
|
2937
|
-
if (
|
|
2946
|
+
function renderDeploymentProfileList(profiles2, format = "pretty") {
|
|
2947
|
+
if (profiles2.length === 0) {
|
|
2938
2948
|
ui.emptyList("deployment profiles");
|
|
2939
2949
|
return;
|
|
2940
2950
|
}
|
|
2941
2951
|
if (format !== "pretty") {
|
|
2942
|
-
const rows =
|
|
2952
|
+
const rows = profiles2.map((p) => ({
|
|
2943
2953
|
name: p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "",
|
|
2944
2954
|
status: p.raw.status ?? "",
|
|
2945
2955
|
authCode: p.raw.auth_code ?? ""
|
|
@@ -2958,7 +2968,7 @@ function renderDeploymentProfileList(profiles, format = "pretty") {
|
|
|
2958
2968
|
return;
|
|
2959
2969
|
}
|
|
2960
2970
|
ui.section("Deployment Profiles:");
|
|
2961
|
-
for (const p of
|
|
2971
|
+
for (const p of profiles2) {
|
|
2962
2972
|
const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
|
|
2963
2973
|
const status = p.raw.status;
|
|
2964
2974
|
const authCode = p.raw.auth_code;
|
|
@@ -3480,7 +3490,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3480
3490
|
const outputError = (error) => controller.abort(error);
|
|
3481
3491
|
let stream;
|
|
3482
3492
|
let failed = false;
|
|
3483
|
-
let
|
|
3493
|
+
let failure2;
|
|
3484
3494
|
try {
|
|
3485
3495
|
const format = await resolveOutput(command, opts, { allowed: ["pretty", "json", "yaml"] });
|
|
3486
3496
|
const config = await loadConfig({ aiGwInferenceEndpoint: opts.endpoint });
|
|
@@ -3507,8 +3517,8 @@ function registerAiGatewayInference(parent) {
|
|
|
3507
3517
|
process.once("SIGINT", interrupt);
|
|
3508
3518
|
process.once("SIGTERM", interrupt);
|
|
3509
3519
|
process.stdout.on("error", outputError);
|
|
3510
|
-
const write = (text2) => new Promise((
|
|
3511
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3520
|
+
const write = (text2) => new Promise((resolve8, reject) => {
|
|
3521
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve8());
|
|
3512
3522
|
});
|
|
3513
3523
|
const requestOptions = { signal: controller.signal };
|
|
3514
3524
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -3543,7 +3553,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3543
3553
|
}
|
|
3544
3554
|
} catch (error) {
|
|
3545
3555
|
failed = true;
|
|
3546
|
-
|
|
3556
|
+
failure2 = error;
|
|
3547
3557
|
} finally {
|
|
3548
3558
|
await stream?.cancel();
|
|
3549
3559
|
controller.abort();
|
|
@@ -3551,10 +3561,10 @@ function registerAiGatewayInference(parent) {
|
|
|
3551
3561
|
process.removeListener("SIGTERM", interrupt);
|
|
3552
3562
|
process.stdout.removeListener("error", outputError);
|
|
3553
3563
|
}
|
|
3554
|
-
if (failed &&
|
|
3555
|
-
if (
|
|
3556
|
-
usageError(
|
|
3557
|
-
fail(
|
|
3564
|
+
if (failed && failure2?.code !== "EPIPE") {
|
|
3565
|
+
if (failure2?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
|
|
3566
|
+
usageError(failure2 instanceof Error ? failure2.message : "Invalid inference options");
|
|
3567
|
+
fail(failure2);
|
|
3558
3568
|
}
|
|
3559
3569
|
});
|
|
3560
3570
|
}
|
|
@@ -3677,19 +3687,19 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3677
3687
|
mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
|
|
3678
3688
|
writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
3679
3689
|
const originalFetch = globalThis.fetch;
|
|
3680
|
-
globalThis.fetch = async function debugFetch(
|
|
3681
|
-
const url = typeof
|
|
3690
|
+
globalThis.fetch = async function debugFetch(input2, init2) {
|
|
3691
|
+
const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
|
|
3682
3692
|
const rawHeaders = headersToRecord(
|
|
3683
|
-
init2?.headers ?? (
|
|
3693
|
+
init2?.headers ?? (input2 instanceof Request ? input2.headers : void 0)
|
|
3684
3694
|
);
|
|
3685
3695
|
const inference = Object.keys(rawHeaders).some(
|
|
3686
3696
|
(name) => name.toLowerCase() === "x-portkey-api-key"
|
|
3687
3697
|
);
|
|
3688
3698
|
if (!isAirsUrl(url) && !inference) {
|
|
3689
|
-
return originalFetch(
|
|
3699
|
+
return originalFetch(input2, init2);
|
|
3690
3700
|
}
|
|
3691
3701
|
const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
|
|
3692
|
-
const method = init2?.method ?? (
|
|
3702
|
+
const method = init2?.method ?? (input2 instanceof Request ? input2.method : "GET");
|
|
3693
3703
|
const reqHeaders = redactHeaders(rawHeaders);
|
|
3694
3704
|
const loggedUrl = redactUrl(url);
|
|
3695
3705
|
let reqBody;
|
|
@@ -3708,7 +3718,7 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3708
3718
|
let resBody;
|
|
3709
3719
|
let error;
|
|
3710
3720
|
try {
|
|
3711
|
-
response = await originalFetch(
|
|
3721
|
+
response = await originalFetch(input2, init2);
|
|
3712
3722
|
} catch (err) {
|
|
3713
3723
|
error = inference ? "Runtime request failed" : "Request failed";
|
|
3714
3724
|
const entry2 = JSON.stringify({
|
|
@@ -3882,20 +3892,20 @@ function renderGatewayDetail(value, format) {
|
|
|
3882
3892
|
);
|
|
3883
3893
|
if (rendered) console.log(rendered);
|
|
3884
3894
|
}
|
|
3885
|
-
async function runList(command, opts, label,
|
|
3895
|
+
async function runList(command, opts, label, load3) {
|
|
3886
3896
|
try {
|
|
3887
3897
|
const format = await resolveOutput(command, opts);
|
|
3888
3898
|
const client = await createAiGatewayClient();
|
|
3889
|
-
renderGatewayList(responseItems(await
|
|
3899
|
+
renderGatewayList(responseItems(await load3(client)), format, label);
|
|
3890
3900
|
} catch (error) {
|
|
3891
3901
|
failAiGateway(error);
|
|
3892
3902
|
}
|
|
3893
3903
|
}
|
|
3894
|
-
async function runDetail(command, opts,
|
|
3904
|
+
async function runDetail(command, opts, load3) {
|
|
3895
3905
|
try {
|
|
3896
3906
|
const format = await resolveOutput(command, opts);
|
|
3897
3907
|
const client = await createAiGatewayClient();
|
|
3898
|
-
renderGatewayDetail(await
|
|
3908
|
+
renderGatewayDetail(await load3(client), format);
|
|
3899
3909
|
} catch (error) {
|
|
3900
3910
|
failAiGateway(error);
|
|
3901
3911
|
}
|
|
@@ -5230,8 +5240,8 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5230
5240
|
const iso = z2.string().datetime({ offset: true });
|
|
5231
5241
|
if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
|
|
5232
5242
|
usageError("Provide a valid increasing ISO-8601 window with timezone");
|
|
5233
|
-
const
|
|
5234
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
5243
|
+
const maxPages2 = Number(opts.maxPages);
|
|
5244
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
5235
5245
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
5236
5246
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
5237
5247
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -5255,7 +5265,7 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5255
5265
|
ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
|
|
5256
5266
|
const report = await collectGatewayEnvironmentReport(client, {
|
|
5257
5267
|
title: opts.title,
|
|
5258
|
-
maxPages,
|
|
5268
|
+
maxPages: maxPages2,
|
|
5259
5269
|
workspace: opts.workspace,
|
|
5260
5270
|
tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
|
|
5261
5271
|
start,
|
|
@@ -5886,8 +5896,8 @@ async function withTimeout(promise, ms) {
|
|
|
5886
5896
|
try {
|
|
5887
5897
|
return await Promise.race([
|
|
5888
5898
|
promise,
|
|
5889
|
-
new Promise((
|
|
5890
|
-
timer = setTimeout(() =>
|
|
5899
|
+
new Promise((resolve8) => {
|
|
5900
|
+
timer = setTimeout(() => resolve8(TIMED_OUT), ms);
|
|
5891
5901
|
})
|
|
5892
5902
|
]);
|
|
5893
5903
|
} finally {
|
|
@@ -6052,8 +6062,8 @@ async function defaultMgmtProbe() {
|
|
|
6052
6062
|
tsgId: config.mgmtTsgId,
|
|
6053
6063
|
tokenEndpoint: config.mgmtTokenEndpoint
|
|
6054
6064
|
});
|
|
6055
|
-
const
|
|
6056
|
-
return
|
|
6065
|
+
const topics2 = await service.listTopics();
|
|
6066
|
+
return topics2.length;
|
|
6057
6067
|
}
|
|
6058
6068
|
async function defaultAiGwProbe() {
|
|
6059
6069
|
const config = await loadConfig();
|
|
@@ -6172,12 +6182,12 @@ function hasBin(bin) {
|
|
|
6172
6182
|
}
|
|
6173
6183
|
}
|
|
6174
6184
|
function run2(bin, args, label) {
|
|
6175
|
-
return new Promise((
|
|
6185
|
+
return new Promise((resolve8, reject) => {
|
|
6176
6186
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
6177
6187
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
6178
6188
|
child.on("close", (code) => {
|
|
6179
6189
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
6180
|
-
else
|
|
6190
|
+
else resolve8();
|
|
6181
6191
|
});
|
|
6182
6192
|
});
|
|
6183
6193
|
}
|
|
@@ -6787,8 +6797,8 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6787
6797
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6788
6798
|
if (format !== "html" && format !== "markdown")
|
|
6789
6799
|
usageError("Report output must be html or markdown");
|
|
6790
|
-
const
|
|
6791
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
6800
|
+
const maxPages2 = Number(opts.maxPages);
|
|
6801
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
6792
6802
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
6793
6803
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
6794
6804
|
usageError("--title must contain 1\u2013240 characters");
|
|
@@ -6812,7 +6822,7 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6812
6822
|
ui.status("Collecting Red Team statistics and current configuration (read-only)...");
|
|
6813
6823
|
const report = await collectRedTeamEnvironmentReport(client, {
|
|
6814
6824
|
title: opts.title,
|
|
6815
|
-
maxPages
|
|
6825
|
+
maxPages: maxPages2
|
|
6816
6826
|
});
|
|
6817
6827
|
const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
|
|
6818
6828
|
if (outputPath) {
|
|
@@ -6948,8 +6958,8 @@ function buildDefaultCategories(categories) {
|
|
|
6948
6958
|
])
|
|
6949
6959
|
);
|
|
6950
6960
|
}
|
|
6951
|
-
function parseAttackGoals(
|
|
6952
|
-
const trimmed =
|
|
6961
|
+
function parseAttackGoals(input2) {
|
|
6962
|
+
const trimmed = input2.trim();
|
|
6953
6963
|
const raw = trimmed.startsWith("[") ? trimmed : fs2.readFileSync(trimmed, "utf-8");
|
|
6954
6964
|
let parsed;
|
|
6955
6965
|
try {
|
|
@@ -6971,10 +6981,10 @@ function sliceClientSide(items, opts) {
|
|
|
6971
6981
|
const limit = opts.limit !== void 0 ? Number.parseInt(String(opts.limit), 10) : void 0;
|
|
6972
6982
|
return items.slice(offset, limit === void 0 ? void 0 : offset + limit);
|
|
6973
6983
|
}
|
|
6974
|
-
function parsePositiveInt(
|
|
6975
|
-
const n = Number.parseInt(
|
|
6984
|
+
function parsePositiveInt(input2, flag) {
|
|
6985
|
+
const n = Number.parseInt(input2, 10);
|
|
6976
6986
|
if (!Number.isFinite(n) || n <= 0) {
|
|
6977
|
-
throw new Error(`${flag}: expected a positive integer, got "${
|
|
6987
|
+
throw new Error(`${flag}: expected a positive integer, got "${input2}"`);
|
|
6978
6988
|
}
|
|
6979
6989
|
return n;
|
|
6980
6990
|
}
|
|
@@ -7060,10 +7070,10 @@ function resolveScriptB64(opts) {
|
|
|
7060
7070
|
}
|
|
7061
7071
|
throw new Error("one of --script-file or --script-b64 is required");
|
|
7062
7072
|
}
|
|
7063
|
-
function parseAdapterVariables(
|
|
7073
|
+
function parseAdapterVariables(input2) {
|
|
7064
7074
|
let parsed;
|
|
7065
7075
|
try {
|
|
7066
|
-
parsed = JSON.parse(
|
|
7076
|
+
parsed = JSON.parse(input2);
|
|
7067
7077
|
} catch (err) {
|
|
7068
7078
|
throw new Error(`--variables: invalid JSON (${err instanceof Error ? err.message : err})`);
|
|
7069
7079
|
}
|
|
@@ -8071,9 +8081,9 @@ function registerRedteamCommand(program) {
|
|
|
8071
8081
|
}
|
|
8072
8082
|
|
|
8073
8083
|
// src/cli/commands/runtime.ts
|
|
8074
|
-
import { randomUUID as
|
|
8084
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
8075
8085
|
import * as fs5 from "fs";
|
|
8076
|
-
import { readFile as
|
|
8086
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
8077
8087
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
8078
8088
|
import chalk11 from "chalk";
|
|
8079
8089
|
|
|
@@ -8137,9 +8147,9 @@ function buildDataProtection(flags) {
|
|
|
8137
8147
|
let hasAny = false;
|
|
8138
8148
|
if (flags.dlpAction) {
|
|
8139
8149
|
const dld = { action: flags.dlpAction };
|
|
8140
|
-
const
|
|
8141
|
-
if (
|
|
8142
|
-
dld.member =
|
|
8150
|
+
const profiles2 = parseList(flags.dlpProfiles);
|
|
8151
|
+
if (profiles2) {
|
|
8152
|
+
dld.member = profiles2.map((text2) => ({ text: text2 }));
|
|
8143
8153
|
}
|
|
8144
8154
|
if (flags.maskDataInline != null) {
|
|
8145
8155
|
dld["mask-data-inline"] = flags.maskDataInline;
|
|
@@ -9525,11 +9535,655 @@ function registerDlpCommands(runtime) {
|
|
|
9525
9535
|
register3(dlp);
|
|
9526
9536
|
}
|
|
9527
9537
|
|
|
9538
|
+
// src/cli/commands/profile-transfer.ts
|
|
9539
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9540
|
+
import { readFile as readFile9, stat } from "fs/promises";
|
|
9541
|
+
import { extname, resolve as resolve5 } from "path";
|
|
9542
|
+
import { AISecSDKException as AISecSDKException2, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9543
|
+
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
9544
|
+
import { ZodError } from "zod";
|
|
9545
|
+
|
|
9546
|
+
// src/backup/runtime-profiles.ts
|
|
9547
|
+
import {
|
|
9548
|
+
CreateSecurityProfileRequestSchema,
|
|
9549
|
+
CustomTopicSchema,
|
|
9550
|
+
PolicySchema,
|
|
9551
|
+
SecurityProfileSchema
|
|
9552
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
9553
|
+
import { z as z5 } from "zod";
|
|
9554
|
+
var RuntimeProfilesBackupSchema = z5.object({
|
|
9555
|
+
version: z5.literal(1),
|
|
9556
|
+
resourceType: z5.literal("runtime-security-profiles"),
|
|
9557
|
+
exportedAt: z5.string().datetime({ offset: true }),
|
|
9558
|
+
source: z5.object({ tsgId: z5.string().min(1) }).strict(),
|
|
9559
|
+
profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
|
|
9560
|
+
topics: z5.array(CustomTopicSchema).max(1e4)
|
|
9561
|
+
}).strict();
|
|
9562
|
+
function object(value) {
|
|
9563
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9564
|
+
}
|
|
9565
|
+
function pagesLimit(value) {
|
|
9566
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1e3)
|
|
9567
|
+
throw new Error("maxPages must be an integer from 1 to 1000");
|
|
9568
|
+
}
|
|
9569
|
+
async function offsetPages(fetch, maxPages2) {
|
|
9570
|
+
pagesLimit(maxPages2);
|
|
9571
|
+
const items = [];
|
|
9572
|
+
let offset = 0;
|
|
9573
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
9574
|
+
const result = await fetch(offset);
|
|
9575
|
+
items.push(...result.items);
|
|
9576
|
+
if (items.length > 1e4) throw new Error("Inventory exceeds the 10000-record safety limit");
|
|
9577
|
+
if (result.next === void 0 || result.next === 0) return items;
|
|
9578
|
+
if (!result.items.length || !Number.isSafeInteger(result.next) || result.next <= offset)
|
|
9579
|
+
throw new Error("Inventory pagination did not advance");
|
|
9580
|
+
offset = result.next;
|
|
9581
|
+
}
|
|
9582
|
+
throw new Error("Inventory is incomplete: increase --max-pages before exporting or restoring");
|
|
9583
|
+
}
|
|
9584
|
+
function latestProfiles(profiles2) {
|
|
9585
|
+
const names = /* @__PURE__ */ new Map();
|
|
9586
|
+
const revisions = /* @__PURE__ */ new Set();
|
|
9587
|
+
for (const profile of profiles2) {
|
|
9588
|
+
assertName(profile.profile_name);
|
|
9589
|
+
const identity2 = JSON.stringify([profile.profile_name, profile.profile_id, profile.revision]);
|
|
9590
|
+
if (revisions.has(identity2)) throw new Error("Inventory contains repeated profile records");
|
|
9591
|
+
revisions.add(identity2);
|
|
9592
|
+
const prior = names.get(profile.profile_name);
|
|
9593
|
+
if (prior && prior.profile_id !== profile.profile_id)
|
|
9594
|
+
throw new Error(`Ambiguous profile name: ${profile.profile_name}`);
|
|
9595
|
+
if (!prior || (profile.revision ?? 0) > (prior.revision ?? 0))
|
|
9596
|
+
names.set(profile.profile_name, profile);
|
|
9597
|
+
}
|
|
9598
|
+
return [...names.values()].sort((a, b) => a.profile_name.localeCompare(b.profile_name));
|
|
9599
|
+
}
|
|
9600
|
+
function assertName(name) {
|
|
9601
|
+
if (!name.trim() || [...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9602
|
+
throw new Error("Profile/topic names must be nonempty and contain no control characters");
|
|
9603
|
+
}
|
|
9604
|
+
function assertTenant(profiles2, tsgId) {
|
|
9605
|
+
if (profiles2.some((profile) => profile.tsg_id != null && String(profile.tsg_id) !== tsgId))
|
|
9606
|
+
throw new Error("Destination/source profile tenant identity does not match the selected TSG");
|
|
9607
|
+
}
|
|
9608
|
+
async function profiles(api, maxPages2) {
|
|
9609
|
+
return latestProfiles(
|
|
9610
|
+
await offsetPages(async (offset) => {
|
|
9611
|
+
const page = await api.profiles.list({ offset, limit: 100, latest: true });
|
|
9612
|
+
return { items: page.ai_profiles, next: page.next_offset };
|
|
9613
|
+
}, maxPages2)
|
|
9614
|
+
);
|
|
9615
|
+
}
|
|
9616
|
+
async function topics(api, maxPages2) {
|
|
9617
|
+
return offsetPages(async (offset) => {
|
|
9618
|
+
const page = await api.topics.list({ offset, limit: 100 });
|
|
9619
|
+
return { items: page.custom_topics, next: page.next_offset };
|
|
9620
|
+
}, maxPages2);
|
|
9621
|
+
}
|
|
9622
|
+
function topicReferences(policy) {
|
|
9623
|
+
const refs = [];
|
|
9624
|
+
const visit = (value) => {
|
|
9625
|
+
if (Array.isArray(value)) {
|
|
9626
|
+
value.forEach(visit);
|
|
9627
|
+
return;
|
|
9628
|
+
}
|
|
9629
|
+
if (!object(value)) return;
|
|
9630
|
+
if ("topic_id" in value) {
|
|
9631
|
+
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9632
|
+
throw new Error("Invalid topic identity in profile policy");
|
|
9633
|
+
refs.push(value);
|
|
9634
|
+
}
|
|
9635
|
+
Object.values(value).forEach(visit);
|
|
9636
|
+
};
|
|
9637
|
+
visit(policy);
|
|
9638
|
+
return refs;
|
|
9639
|
+
}
|
|
9640
|
+
function topicKey(ref) {
|
|
9641
|
+
return JSON.stringify([ref.topic_id, ref.revision]);
|
|
9642
|
+
}
|
|
9643
|
+
async function backupRuntimeProfiles(api, tsgId, options = {}) {
|
|
9644
|
+
if (!tsgId) throw new Error("Source TSG ID is required");
|
|
9645
|
+
const maxPages2 = options.maxPages ?? 100;
|
|
9646
|
+
let selected = await profiles(api, maxPages2);
|
|
9647
|
+
assertTenant(selected, tsgId);
|
|
9648
|
+
if (options.profile)
|
|
9649
|
+
selected = selected.filter(
|
|
9650
|
+
(p) => p.profile_id === options.profile || p.profile_name === options.profile
|
|
9651
|
+
);
|
|
9652
|
+
if (!selected.length) throw new Error("No matching profiles to back up");
|
|
9653
|
+
if (options.profile && selected.length !== 1) throw new Error("Ambiguous profile selector");
|
|
9654
|
+
for (const profile of selected) {
|
|
9655
|
+
if (!profile.policy) throw new Error(`Profile has no policy: ${profile.profile_name}`);
|
|
9656
|
+
PolicySchema.parse(profile.policy);
|
|
9657
|
+
}
|
|
9658
|
+
const refs = selected.flatMap((profile) => topicReferences(profile.policy));
|
|
9659
|
+
const definitions = refs.length ? await topics(api, maxPages2) : [];
|
|
9660
|
+
const needed = /* @__PURE__ */ new Map();
|
|
9661
|
+
for (const ref of refs) {
|
|
9662
|
+
const matches = definitions.filter((topic) => topicKey(topic) === topicKey(ref));
|
|
9663
|
+
if (matches.length !== 1 || matches[0].topic_name !== ref.topic_name)
|
|
9664
|
+
throw new Error(
|
|
9665
|
+
`Cannot export the exact referenced topic revision: ${ref.topic_name} revision ${ref.revision}`
|
|
9666
|
+
);
|
|
9667
|
+
needed.set(topicKey(ref), matches[0]);
|
|
9668
|
+
}
|
|
9669
|
+
return RuntimeProfilesBackupSchema.parse({
|
|
9670
|
+
version: 1,
|
|
9671
|
+
resourceType: "runtime-security-profiles",
|
|
9672
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9673
|
+
source: { tsgId },
|
|
9674
|
+
profiles: selected,
|
|
9675
|
+
topics: [...needed.values()]
|
|
9676
|
+
});
|
|
9677
|
+
}
|
|
9678
|
+
function sameTopic(a, b) {
|
|
9679
|
+
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9680
|
+
}
|
|
9681
|
+
function dlpReferences(policy) {
|
|
9682
|
+
if (!object(policy)) return [];
|
|
9683
|
+
const refs = [];
|
|
9684
|
+
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9685
|
+
if (!object(entry) || typeof entry.name !== "string")
|
|
9686
|
+
throw new Error("Invalid embedded DLP reference");
|
|
9687
|
+
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9688
|
+
}
|
|
9689
|
+
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9690
|
+
if (!object(entry) || !object(entry["model-configuration"])) continue;
|
|
9691
|
+
const protection = entry["model-configuration"]["data-protection"];
|
|
9692
|
+
if (!object(protection) || !object(protection["data-leak-detection"])) continue;
|
|
9693
|
+
const members = protection["data-leak-detection"].member;
|
|
9694
|
+
if (!Array.isArray(members)) continue;
|
|
9695
|
+
for (const member of members) {
|
|
9696
|
+
if (!object(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9697
|
+
refs.push({ value: member, name: member.text, kind: "member" });
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
return refs;
|
|
9701
|
+
}
|
|
9702
|
+
function assertKnownPolicyReferences(policy) {
|
|
9703
|
+
const known = new Map([
|
|
9704
|
+
...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
|
|
9705
|
+
...dlpReferences(policy).map((ref) => [
|
|
9706
|
+
ref.value,
|
|
9707
|
+
ref.kind === "embedded" ? ["id", "uuid"] : ["id"]
|
|
9708
|
+
])
|
|
9709
|
+
]);
|
|
9710
|
+
const visit = (value, path3) => {
|
|
9711
|
+
if (Array.isArray(value)) {
|
|
9712
|
+
value.forEach((v, i) => {
|
|
9713
|
+
visit(v, `${path3}[${i}]`);
|
|
9714
|
+
});
|
|
9715
|
+
return;
|
|
9716
|
+
}
|
|
9717
|
+
if (!object(value)) return;
|
|
9718
|
+
for (const [key, child] of Object.entries(value)) {
|
|
9719
|
+
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9720
|
+
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
9721
|
+
visit(child, `${path3}.${key}`);
|
|
9722
|
+
}
|
|
9723
|
+
};
|
|
9724
|
+
visit(policy, "policy");
|
|
9725
|
+
}
|
|
9726
|
+
async function dataProfiles(api, maxPages2) {
|
|
9727
|
+
const items = [];
|
|
9728
|
+
for (let page = 0; page < maxPages2; page++) {
|
|
9729
|
+
const result = await api.dataProfiles.list({ page, size: 100 });
|
|
9730
|
+
items.push(...result.content);
|
|
9731
|
+
if (items.length > 1e4) throw new Error("DLP inventory exceeds the safety limit");
|
|
9732
|
+
const last = result.last ?? (result.totalPages !== void 0 ? page + 1 >= result.totalPages : result.content.length < 100);
|
|
9733
|
+
if (last) return items;
|
|
9734
|
+
if (!result.content.length) throw new Error("DLP inventory pagination did not advance");
|
|
9735
|
+
}
|
|
9736
|
+
throw new Error("DLP inventory is incomplete; increase --max-pages");
|
|
9737
|
+
}
|
|
9738
|
+
async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9739
|
+
const backup = RuntimeProfilesBackupSchema.parse(input2);
|
|
9740
|
+
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9741
|
+
const maxPages2 = options.maxPages ?? 100;
|
|
9742
|
+
pagesLimit(maxPages2);
|
|
9743
|
+
const prefix = options.namePrefix ?? "";
|
|
9744
|
+
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9745
|
+
throw new Error("Invalid --name-prefix");
|
|
9746
|
+
const conflict = options.onConflict ?? "error";
|
|
9747
|
+
if (!["error", "skip", "update"].includes(conflict)) throw new Error("Invalid conflict policy");
|
|
9748
|
+
const selected = latestProfiles(backup.profiles);
|
|
9749
|
+
assertTenant(selected, backup.source.tsgId);
|
|
9750
|
+
if (selected.length !== backup.profiles.length)
|
|
9751
|
+
throw new Error("Backup contains duplicate profile names/revisions");
|
|
9752
|
+
const crossTenant = backup.source.tsgId !== destinationTsgId;
|
|
9753
|
+
for (const profile of selected) {
|
|
9754
|
+
if (!profile.policy) throw new Error("Backup profile has no policy");
|
|
9755
|
+
CreateSecurityProfileRequestSchema.parse({
|
|
9756
|
+
profile_name: `${prefix}${profile.profile_name}`,
|
|
9757
|
+
active: profile.active,
|
|
9758
|
+
policy: profile.policy
|
|
9759
|
+
});
|
|
9760
|
+
if (crossTenant) assertKnownPolicyReferences(profile.policy);
|
|
9761
|
+
}
|
|
9762
|
+
const existing = await profiles(api, maxPages2);
|
|
9763
|
+
assertTenant(existing, destinationTsgId);
|
|
9764
|
+
const profilePlans = selected.map((source) => {
|
|
9765
|
+
const name = `${prefix}${source.profile_name}`;
|
|
9766
|
+
const match = existing.find((p) => p.profile_name === name);
|
|
9767
|
+
if (match && conflict === "error")
|
|
9768
|
+
throw new Error(
|
|
9769
|
+
`Destination profile already exists: ${name}; choose --on-conflict skip or update`
|
|
9770
|
+
);
|
|
9771
|
+
if (match && !match.profile_id) throw new Error("Destination profile has no identity");
|
|
9772
|
+
return {
|
|
9773
|
+
source: structuredClone(source),
|
|
9774
|
+
name,
|
|
9775
|
+
existing: match,
|
|
9776
|
+
action: match ? conflict === "skip" ? "skip" : "update" : "create"
|
|
9777
|
+
};
|
|
9778
|
+
});
|
|
9779
|
+
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
9780
|
+
const refs = changing.flatMap((p) => topicReferences(p.source.policy));
|
|
9781
|
+
const targetTopics = refs.length ? await topics(api, maxPages2) : [];
|
|
9782
|
+
const topicPlans = /* @__PURE__ */ new Map();
|
|
9783
|
+
const topicNames = /* @__PURE__ */ new Map();
|
|
9784
|
+
for (const ref of refs) {
|
|
9785
|
+
const key = topicKey(ref);
|
|
9786
|
+
if (topicPlans.has(key)) continue;
|
|
9787
|
+
const definitions = backup.topics.filter(
|
|
9788
|
+
(topic) => topicKey(topic) === key && topic.topic_name === ref.topic_name
|
|
9789
|
+
);
|
|
9790
|
+
if (definitions.length !== 1)
|
|
9791
|
+
throw new Error(`Backup lacks an unambiguous topic definition: ${ref.topic_name}`);
|
|
9792
|
+
const source = definitions[0];
|
|
9793
|
+
const name = `${prefix}${source.topic_name}`;
|
|
9794
|
+
assertName(name);
|
|
9795
|
+
if (topicNames.has(name) && topicNames.get(name) !== key)
|
|
9796
|
+
throw new Error(
|
|
9797
|
+
`Multiple source revisions use topic name ${name}; split the backup by profile/revision before restoring`
|
|
9798
|
+
);
|
|
9799
|
+
topicNames.set(name, key);
|
|
9800
|
+
const matches = targetTopics.filter((t) => t.topic_name === name).sort((a, b) => b.revision - a.revision);
|
|
9801
|
+
const match = matches[0];
|
|
9802
|
+
if (new Set(matches.map((t) => t.topic_id)).size > 1)
|
|
9803
|
+
throw new Error(`Ambiguous destination topic name: ${name}`);
|
|
9804
|
+
if (match && (!match.topic_id || !sameTopic(source, match)))
|
|
9805
|
+
throw new Error(
|
|
9806
|
+
`Destination topic differs: ${name}; use --name-prefix to avoid changing a shared topic`
|
|
9807
|
+
);
|
|
9808
|
+
topicPlans.set(key, { key, name, source, existing: match });
|
|
9809
|
+
}
|
|
9810
|
+
const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9811
|
+
const names = [...new Set(dlpRefs.map((ref) => ref.name))];
|
|
9812
|
+
for (const name of names)
|
|
9813
|
+
if (!options.dlpMap?.[name])
|
|
9814
|
+
throw new Error(
|
|
9815
|
+
`Cross-tenant DLP reference requires explicit --dlp-map ${name}=<destination-name>; DLP resources are not cloned`
|
|
9816
|
+
);
|
|
9817
|
+
const catalog = names.length ? await dataProfiles(api, maxPages2) : [];
|
|
9818
|
+
const dlpMappings = names.map((name) => {
|
|
9819
|
+
const target = options.dlpMap?.[name];
|
|
9820
|
+
const matches = catalog.filter((p) => p.name === target && p.profile_status !== "deleted");
|
|
9821
|
+
if (matches.length !== 1 || !matches[0].id)
|
|
9822
|
+
throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
|
|
9823
|
+
if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
|
|
9824
|
+
throw new Error(`Destination DLP version is unknown: ${target}`);
|
|
9825
|
+
return {
|
|
9826
|
+
source: name,
|
|
9827
|
+
destination: String(target),
|
|
9828
|
+
id: matches[0].id,
|
|
9829
|
+
version: matches[0].version ?? void 0
|
|
9830
|
+
};
|
|
9831
|
+
});
|
|
9832
|
+
return {
|
|
9833
|
+
sourceTsgId: backup.source.tsgId,
|
|
9834
|
+
destinationTsgId,
|
|
9835
|
+
profiles: profilePlans,
|
|
9836
|
+
topics: [...topicPlans.values()],
|
|
9837
|
+
dlpMappings,
|
|
9838
|
+
maxPages: maxPages2
|
|
9839
|
+
};
|
|
9840
|
+
}
|
|
9841
|
+
function canonical(value) {
|
|
9842
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object(v) ? Object.fromEntries(
|
|
9843
|
+
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9844
|
+
) : v;
|
|
9845
|
+
return JSON.stringify(order(value));
|
|
9846
|
+
}
|
|
9847
|
+
function unchanged(current, prior) {
|
|
9848
|
+
return Boolean(
|
|
9849
|
+
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonical(current.policy) === canonical(prior.policy)
|
|
9850
|
+
);
|
|
9851
|
+
}
|
|
9852
|
+
async function restoreRuntimeProfiles(api, plan) {
|
|
9853
|
+
const result = {
|
|
9854
|
+
sourceTsgId: plan.sourceTsgId,
|
|
9855
|
+
destinationTsgId: plan.destinationTsgId,
|
|
9856
|
+
complete: false,
|
|
9857
|
+
topics: [],
|
|
9858
|
+
profiles: []
|
|
9859
|
+
};
|
|
9860
|
+
try {
|
|
9861
|
+
const latest = await profiles(api, plan.maxPages);
|
|
9862
|
+
assertTenant(latest, plan.destinationTsgId);
|
|
9863
|
+
if (plan.dlpMappings.length) {
|
|
9864
|
+
const catalog = await dataProfiles(api, plan.maxPages);
|
|
9865
|
+
for (const mapping of plan.dlpMappings) {
|
|
9866
|
+
const matches = catalog.filter(
|
|
9867
|
+
(p) => p.name === mapping.destination && p.profile_status !== "deleted"
|
|
9868
|
+
);
|
|
9869
|
+
if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
|
|
9870
|
+
throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
for (const item of plan.profiles) {
|
|
9874
|
+
if (item.action === "skip") continue;
|
|
9875
|
+
const current = latest.find((p) => p.profile_name === item.name);
|
|
9876
|
+
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9877
|
+
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
9878
|
+
}
|
|
9879
|
+
const mappedTopics = /* @__PURE__ */ new Map();
|
|
9880
|
+
for (const item of plan.topics) {
|
|
9881
|
+
let topic = item.existing;
|
|
9882
|
+
const inventory2 = await topics(api, plan.maxPages);
|
|
9883
|
+
if (topic) {
|
|
9884
|
+
const current = inventory2.find(
|
|
9885
|
+
(t) => t.topic_id === topic?.topic_id && t.revision === topic?.revision
|
|
9886
|
+
);
|
|
9887
|
+
if (!current || !sameTopic(current, item.source))
|
|
9888
|
+
throw new Error(`Destination topic changed after planning: ${item.name}`);
|
|
9889
|
+
} else {
|
|
9890
|
+
if (inventory2.some((t) => t.topic_name === item.name))
|
|
9891
|
+
throw new Error(`Destination topic changed after planning: ${item.name}`);
|
|
9892
|
+
topic = await api.topics.create({
|
|
9893
|
+
topic_name: item.name,
|
|
9894
|
+
description: item.source.description,
|
|
9895
|
+
examples: item.source.examples,
|
|
9896
|
+
active: item.source.active
|
|
9897
|
+
});
|
|
9898
|
+
}
|
|
9899
|
+
if (!topic.topic_id || topic.topic_name !== item.name || !Number.isSafeInteger(topic.revision) || !sameTopic(topic, item.source))
|
|
9900
|
+
throw new Error(`Topic response did not verify: ${item.name}`);
|
|
9901
|
+
mappedTopics.set(item.key, topic);
|
|
9902
|
+
result.topics.push({
|
|
9903
|
+
name: item.name,
|
|
9904
|
+
id: topic.topic_id,
|
|
9905
|
+
action: item.existing ? "reused" : "created"
|
|
9906
|
+
});
|
|
9907
|
+
}
|
|
9908
|
+
for (const item of plan.profiles) {
|
|
9909
|
+
if (item.action === "skip") {
|
|
9910
|
+
result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
|
|
9911
|
+
continue;
|
|
9912
|
+
}
|
|
9913
|
+
const policy = structuredClone(item.source.policy);
|
|
9914
|
+
for (const ref of topicReferences(policy)) {
|
|
9915
|
+
const topic = mappedTopics.get(topicKey(ref));
|
|
9916
|
+
if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
|
|
9917
|
+
ref.topic_id = topic.topic_id;
|
|
9918
|
+
ref.topic_name = topic.topic_name;
|
|
9919
|
+
ref.revision = topic.revision;
|
|
9920
|
+
}
|
|
9921
|
+
for (const ref of dlpReferences(policy)) {
|
|
9922
|
+
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
9923
|
+
if (!mapping) continue;
|
|
9924
|
+
if (ref.kind === "embedded") {
|
|
9925
|
+
ref.value.name = mapping.destination;
|
|
9926
|
+
ref.value.uuid = mapping.id;
|
|
9927
|
+
} else ref.value.text = mapping.destination;
|
|
9928
|
+
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
9929
|
+
if ("version" in ref.value) {
|
|
9930
|
+
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
9931
|
+
ref.value.version = String(mapping.version);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
const body = CreateSecurityProfileRequestSchema.parse({
|
|
9935
|
+
profile_name: item.name,
|
|
9936
|
+
active: item.source.active,
|
|
9937
|
+
policy
|
|
9938
|
+
});
|
|
9939
|
+
const inventory2 = await profiles(api, plan.maxPages);
|
|
9940
|
+
assertTenant(inventory2, plan.destinationTsgId);
|
|
9941
|
+
const current = inventory2.find((p) => p.profile_name === item.name);
|
|
9942
|
+
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9943
|
+
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
9944
|
+
const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
|
|
9945
|
+
result.profiles.push({
|
|
9946
|
+
name: item.name,
|
|
9947
|
+
action: item.action === "create" ? "created" : "updated",
|
|
9948
|
+
id: response.profile_id
|
|
9949
|
+
});
|
|
9950
|
+
const verified = (await profiles(api, plan.maxPages)).find(
|
|
9951
|
+
(p) => p.profile_name === item.name
|
|
9952
|
+
);
|
|
9953
|
+
if (verified) assertTenant([verified], plan.destinationTsgId);
|
|
9954
|
+
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))
|
|
9955
|
+
throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
9956
|
+
}
|
|
9957
|
+
result.complete = true;
|
|
9958
|
+
} catch (error) {
|
|
9959
|
+
const status = error?.statusCode;
|
|
9960
|
+
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(
|
|
9961
|
+
error.message
|
|
9962
|
+
) ? error.message : "Restore stopped; verify destination state before retrying";
|
|
9963
|
+
}
|
|
9964
|
+
return result;
|
|
9965
|
+
}
|
|
9966
|
+
|
|
9967
|
+
// src/cli/commands/profile-transfer.ts
|
|
9968
|
+
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
9969
|
+
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
9970
|
+
function maxPages(value) {
|
|
9971
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
9972
|
+
usageError("--max-pages must be an integer from 1 to 1000");
|
|
9973
|
+
return Number(value);
|
|
9974
|
+
}
|
|
9975
|
+
function dlpMap(values) {
|
|
9976
|
+
const entries = values.map((value) => {
|
|
9977
|
+
const at = value.indexOf("=");
|
|
9978
|
+
if (at < 1 || at === value.length - 1)
|
|
9979
|
+
throw new CliUsageError("--dlp-map requires source-name=destination-name");
|
|
9980
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
9981
|
+
});
|
|
9982
|
+
if (new Set(entries.map(([key]) => key)).size !== entries.length)
|
|
9983
|
+
throw new CliUsageError("Duplicate source names in --dlp-map");
|
|
9984
|
+
return Object.fromEntries(entries);
|
|
9985
|
+
}
|
|
9986
|
+
async function context() {
|
|
9987
|
+
const config = await loadConfig();
|
|
9988
|
+
if (!config.mgmtTsgId || !config.mgmtClientId || !config.mgmtClientSecret)
|
|
9989
|
+
throw new CliUsageError(
|
|
9990
|
+
"Profile transfer requires configured Management client ID, secret and TSG ID"
|
|
9991
|
+
);
|
|
9992
|
+
const client = new ManagementClient({ ...managementClientOptions(config), numRetries: 0 });
|
|
9993
|
+
return {
|
|
9994
|
+
api: {
|
|
9995
|
+
profiles: client.profiles,
|
|
9996
|
+
topics: client.topics,
|
|
9997
|
+
dataProfiles: client.dlp.dataProfiles
|
|
9998
|
+
},
|
|
9999
|
+
tsgId: config.mgmtTsgId
|
|
10000
|
+
};
|
|
10001
|
+
}
|
|
10002
|
+
function failure(error) {
|
|
10003
|
+
if (error instanceof CliUsageError) usageError(error.message);
|
|
10004
|
+
if (error instanceof ZodError) {
|
|
10005
|
+
ui.error("Invalid profile backup/request schema; content is not logged.");
|
|
10006
|
+
} else if (error instanceof AISecSDKException2) {
|
|
10007
|
+
ui.error(
|
|
10008
|
+
`Profile transfer API request failed${error.statusCode ? ` (HTTP ${error.statusCode})` : ""}; no automatic retry was made.`
|
|
10009
|
+
);
|
|
10010
|
+
} else if (error?.code) {
|
|
10011
|
+
ui.error(
|
|
10012
|
+
`Profile transfer file operation failed (${error.code}); existing files are not overwritten.`
|
|
10013
|
+
);
|
|
10014
|
+
} else {
|
|
10015
|
+
ui.error(error instanceof Error ? error.message : "Profile transfer failed");
|
|
10016
|
+
}
|
|
10017
|
+
process.exitCode = 1;
|
|
10018
|
+
}
|
|
10019
|
+
function registerProfileTransferCommands(profiles2) {
|
|
10020
|
+
const backup = profiles2.command("backup [profile]").description(
|
|
10021
|
+
"Back up latest Runtime profile policies and referenced topic definitions; no argument means all profiles"
|
|
10022
|
+
).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(
|
|
10023
|
+
"--max-pages <n>",
|
|
10024
|
+
"Maximum pages per inventory; incomplete exports fail",
|
|
10025
|
+
maxPages,
|
|
10026
|
+
100
|
|
10027
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10028
|
+
"after",
|
|
10029
|
+
examples(
|
|
10030
|
+
"airs runtime profiles backup --all --output-file ./profiles.json",
|
|
10031
|
+
'airs runtime profiles backup "Production" --file-format yaml --output-file ./production.yaml'
|
|
10032
|
+
)
|
|
10033
|
+
).action(async (profile, opts) => {
|
|
10034
|
+
try {
|
|
10035
|
+
if (opts.all && profile)
|
|
10036
|
+
throw new CliUsageError("Choose either a profile selector or --all");
|
|
10037
|
+
if (!["json", "yaml"].includes(opts.fileFormat))
|
|
10038
|
+
throw new CliUsageError("--file-format must be json or yaml");
|
|
10039
|
+
const format = await resolveOutput(backup, opts);
|
|
10040
|
+
const extension = opts.fileFormat === "yaml" ? ".yaml" : ".json";
|
|
10041
|
+
if (opts.outputFile === "-")
|
|
10042
|
+
throw new CliUsageError("Backups require a private file destination, not stdout");
|
|
10043
|
+
const path3 = resolve5(
|
|
10044
|
+
opts.outputFile ?? `airs-runtime-profiles-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID7().slice(0, 8)}${extension}`
|
|
10045
|
+
);
|
|
10046
|
+
if (!(opts.fileFormat === "json" ? [".json"] : [".yaml", ".yml"]).includes(
|
|
10047
|
+
extname(path3).toLowerCase()
|
|
10048
|
+
))
|
|
10049
|
+
throw new CliUsageError("Backup filename extension must match --file-format");
|
|
10050
|
+
const { api, tsgId } = await context();
|
|
10051
|
+
const data = await backupRuntimeProfiles(api, tsgId, { profile, maxPages: opts.maxPages });
|
|
10052
|
+
const content = opts.fileFormat === "yaml" ? dump6(data, { schema: JSON_SCHEMA, noRefs: true, lineWidth: -1 }) : `${JSON.stringify(data, null, 2)}
|
|
10053
|
+
`;
|
|
10054
|
+
if (Buffer.byteLength(content) > MAX_BACKUP_BYTES)
|
|
10055
|
+
throw new Error("Backup exceeds the 20 MiB size limit; select individual profiles");
|
|
10056
|
+
await writeReportFile(path3, content);
|
|
10057
|
+
console.log(
|
|
10058
|
+
formatOutput(
|
|
10059
|
+
[
|
|
10060
|
+
{
|
|
10061
|
+
file: path3,
|
|
10062
|
+
sourceTsgId: tsgId,
|
|
10063
|
+
profiles: data.profiles.length,
|
|
10064
|
+
topics: data.topics.length
|
|
10065
|
+
}
|
|
10066
|
+
],
|
|
10067
|
+
[
|
|
10068
|
+
{ key: "file", label: "Backup file" },
|
|
10069
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10070
|
+
{ key: "profiles", label: "Profiles" },
|
|
10071
|
+
{ key: "topics", label: "Topics" }
|
|
10072
|
+
],
|
|
10073
|
+
format === "pretty" ? "table" : format
|
|
10074
|
+
)
|
|
10075
|
+
);
|
|
10076
|
+
} catch (error) {
|
|
10077
|
+
failure(error);
|
|
10078
|
+
}
|
|
10079
|
+
});
|
|
10080
|
+
const restore = profiles2.command("restore <file>").description(
|
|
10081
|
+
"Restore profile policies into the selected tenant; validate all dependencies before changes"
|
|
10082
|
+
).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(
|
|
10083
|
+
"--dlp-map <source=destination>",
|
|
10084
|
+
"Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
|
|
10085
|
+
(value, prior) => [...prior, value],
|
|
10086
|
+
[]
|
|
10087
|
+
).option(
|
|
10088
|
+
"--max-pages <n>",
|
|
10089
|
+
"Maximum pages per inventory; incomplete inventories fail",
|
|
10090
|
+
maxPages,
|
|
10091
|
+
100
|
|
10092
|
+
).option("--expect-tsg <id>", "Assert destination tenant ID; required with --force").option(
|
|
10093
|
+
"--force",
|
|
10094
|
+
"Execute without a prompt; requires --expect-tsg and does not override conflicts"
|
|
10095
|
+
).option("--output <format>", FORMATS).addHelpText(
|
|
10096
|
+
"after",
|
|
10097
|
+
examples(
|
|
10098
|
+
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
10099
|
+
"airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10100
|
+
)
|
|
10101
|
+
).action(async (file, opts) => {
|
|
10102
|
+
try {
|
|
10103
|
+
if (!["error", "skip", "update"].includes(opts.onConflict))
|
|
10104
|
+
throw new CliUsageError("--on-conflict must be error, skip or update");
|
|
10105
|
+
if (opts.force && !opts.expectTsg)
|
|
10106
|
+
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10107
|
+
const mappings = dlpMap(opts.dlpMap);
|
|
10108
|
+
const path3 = resolve5(file);
|
|
10109
|
+
const extension = extname(path3).toLowerCase();
|
|
10110
|
+
if (![".json", ".yaml", ".yml"].includes(extension))
|
|
10111
|
+
throw new CliUsageError("Backup file must use .json, .yaml or .yml");
|
|
10112
|
+
const info = await stat(path3);
|
|
10113
|
+
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10114
|
+
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10115
|
+
let input2;
|
|
10116
|
+
try {
|
|
10117
|
+
const text2 = await readFile9(path3, "utf8");
|
|
10118
|
+
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10119
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10120
|
+
} catch {
|
|
10121
|
+
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10122
|
+
}
|
|
10123
|
+
const data = RuntimeProfilesBackupSchema.parse(input2);
|
|
10124
|
+
const format = await resolveOutput(restore, opts);
|
|
10125
|
+
const { api, tsgId } = await context();
|
|
10126
|
+
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
10127
|
+
throw new CliUsageError(
|
|
10128
|
+
"--expect-tsg does not match the selected destination tenant; no API request was made"
|
|
10129
|
+
);
|
|
10130
|
+
const plan = await planRuntimeProfilesRestore(api, data, tsgId, {
|
|
10131
|
+
namePrefix: opts.namePrefix,
|
|
10132
|
+
onConflict: opts.onConflict,
|
|
10133
|
+
maxPages: opts.maxPages,
|
|
10134
|
+
dlpMap: mappings
|
|
10135
|
+
});
|
|
10136
|
+
const summary = {
|
|
10137
|
+
sourceTsgId: plan.sourceTsgId,
|
|
10138
|
+
destinationTsgId: plan.destinationTsgId,
|
|
10139
|
+
dryRun: Boolean(opts.dryRun),
|
|
10140
|
+
profiles: plan.profiles.map((p) => ({ name: p.name, action: p.action })),
|
|
10141
|
+
topics: plan.topics.map((t) => ({
|
|
10142
|
+
name: t.name,
|
|
10143
|
+
action: t.existing ? "reuse" : "create"
|
|
10144
|
+
})),
|
|
10145
|
+
dlpMappings: plan.dlpMappings.map((m) => ({
|
|
10146
|
+
source: m.source,
|
|
10147
|
+
destination: m.destination
|
|
10148
|
+
}))
|
|
10149
|
+
};
|
|
10150
|
+
const columns = [
|
|
10151
|
+
{ key: "sourceTsgId", label: "Source TSG" },
|
|
10152
|
+
{ key: "destinationTsgId", label: "Destination TSG" },
|
|
10153
|
+
{ key: "profiles", label: "Profiles" },
|
|
10154
|
+
{ key: "topics", label: "Topics" },
|
|
10155
|
+
{ key: "dlpMappings", label: "DLP mappings" },
|
|
10156
|
+
{ key: "dryRun", label: "Dry run" },
|
|
10157
|
+
{ key: "complete", label: "Complete" }
|
|
10158
|
+
];
|
|
10159
|
+
if (opts.dryRun) {
|
|
10160
|
+
console.log(formatOutput([summary], columns, format === "pretty" ? "table" : format));
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
10163
|
+
const changing = plan.profiles.filter((p) => p.action !== "skip");
|
|
10164
|
+
if (changing.length)
|
|
10165
|
+
await confirmOrAbort(
|
|
10166
|
+
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates)?`,
|
|
10167
|
+
Boolean(opts.force),
|
|
10168
|
+
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10169
|
+
);
|
|
10170
|
+
const result = await restoreRuntimeProfiles(api, plan);
|
|
10171
|
+
console.log(formatOutput([{ ...result }], columns, format === "pretty" ? "table" : format));
|
|
10172
|
+
if (!result.complete) {
|
|
10173
|
+
ui.error(result.error ?? "Restore incomplete");
|
|
10174
|
+
process.exitCode = 1;
|
|
10175
|
+
}
|
|
10176
|
+
} catch (error) {
|
|
10177
|
+
failure(error);
|
|
10178
|
+
}
|
|
10179
|
+
});
|
|
10180
|
+
}
|
|
10181
|
+
|
|
9528
10182
|
// src/cli/commands/profiles-cleanup.ts
|
|
9529
10183
|
import { execSync } from "child_process";
|
|
9530
|
-
function findDuplicateProfiles(
|
|
10184
|
+
function findDuplicateProfiles(profiles2) {
|
|
9531
10185
|
const groups = /* @__PURE__ */ new Map();
|
|
9532
|
-
for (const p of
|
|
10186
|
+
for (const p of profiles2) {
|
|
9533
10187
|
const rev = p.revision ?? 0;
|
|
9534
10188
|
const entry = { id: p.profileId, revision: rev };
|
|
9535
10189
|
const existing = groups.get(p.profileName);
|
|
@@ -9628,8 +10282,8 @@ function registerCleanupCommand(parent) {
|
|
|
9628
10282
|
|
|
9629
10283
|
// src/cli/commands/runtime-dashboard.ts
|
|
9630
10284
|
import { lstat as lstat4 } from "fs/promises";
|
|
9631
|
-
import { resolve as
|
|
9632
|
-
import { dump as
|
|
10285
|
+
import { resolve as resolve6 } from "path";
|
|
10286
|
+
import { dump as dump7 } from "js-yaml";
|
|
9633
10287
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
9634
10288
|
function integer3(value, name, minimum = 0) {
|
|
9635
10289
|
const n = Number(value);
|
|
@@ -9704,7 +10358,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9704
10358
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9705
10359
|
);
|
|
9706
10360
|
if (opts.outputFile) {
|
|
9707
|
-
const path3 =
|
|
10361
|
+
const path3 = resolve6(text(opts.outputFile, "--output-file"));
|
|
9708
10362
|
const existing = await lstat4(path3).catch((error) => {
|
|
9709
10363
|
if (error.code !== "ENOENT") throw error;
|
|
9710
10364
|
return void 0;
|
|
@@ -9718,9 +10372,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9718
10372
|
numRetries: 0
|
|
9719
10373
|
});
|
|
9720
10374
|
const result = await read(client.dashboard);
|
|
9721
|
-
const output = format === "yaml" ?
|
|
10375
|
+
const output = format === "yaml" ? dump7(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9722
10376
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9723
|
-
await writeReportFile(
|
|
10377
|
+
await writeReportFile(resolve6(String(opts.outputFile)), `${output}
|
|
9724
10378
|
`);
|
|
9725
10379
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9726
10380
|
} else console.log(output);
|
|
@@ -9892,8 +10546,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9892
10546
|
|
|
9893
10547
|
// src/cli/commands/runtime-report.ts
|
|
9894
10548
|
import { lstat as lstat5 } from "fs/promises";
|
|
9895
|
-
import { resolve as
|
|
9896
|
-
import { randomUUID as
|
|
10549
|
+
import { resolve as resolve7 } from "path";
|
|
10550
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
9897
10551
|
function registerRuntimeReportCommand(runtime) {
|
|
9898
10552
|
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
10553
|
"--output-file <path>",
|
|
@@ -9910,15 +10564,15 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9910
10564
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
9911
10565
|
if (format !== "html" && format !== "markdown")
|
|
9912
10566
|
usageError("Report output must be html or markdown");
|
|
9913
|
-
const
|
|
9914
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
10567
|
+
const maxPages2 = Number(opts.maxPages);
|
|
10568
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
9915
10569
|
usageError("--max-pages must be an integer from 1 to 100");
|
|
9916
10570
|
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
9917
10571
|
usageError("--title must contain 1\u2013240 characters");
|
|
9918
10572
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9919
10573
|
usageError("--output-file cannot be empty");
|
|
9920
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9921
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
10574
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID8().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
10575
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve7(opts.outputFile ?? defaultName);
|
|
9922
10576
|
try {
|
|
9923
10577
|
if (outputPath) {
|
|
9924
10578
|
const existing = await lstat5(outputPath).catch((error) => {
|
|
@@ -9933,7 +10587,7 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9933
10587
|
numRetries: 0
|
|
9934
10588
|
});
|
|
9935
10589
|
ui.status("Collecting daily runtime activity and current configuration (read-only)...");
|
|
9936
|
-
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages });
|
|
10590
|
+
const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages2 });
|
|
9937
10591
|
const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
|
|
9938
10592
|
if (outputPath) {
|
|
9939
10593
|
await writeReportFile(outputPath, rendered);
|
|
@@ -9957,26 +10611,26 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9957
10611
|
}
|
|
9958
10612
|
|
|
9959
10613
|
// src/cli/commands/topics-apply.ts
|
|
9960
|
-
async function applyTopicToProfile(mgmt,
|
|
10614
|
+
async function applyTopicToProfile(mgmt, input2) {
|
|
9961
10615
|
const allTopics = await mgmt.listTopics();
|
|
9962
|
-
const match = allTopics.find((t) => t.topic_name ===
|
|
10616
|
+
const match = allTopics.find((t) => t.topic_name === input2.topicName);
|
|
9963
10617
|
if (!match || !match.topic_id) {
|
|
9964
|
-
throw new Error(`Topic "${
|
|
10618
|
+
throw new Error(`Topic "${input2.topicName}" not found. Create it first with "topics create".`);
|
|
9965
10619
|
}
|
|
9966
|
-
const existing = await mgmt.getProfileTopics(
|
|
9967
|
-
const merged = existing.filter((t) => t.topicName !==
|
|
10620
|
+
const existing = await mgmt.getProfileTopics(input2.profileName);
|
|
10621
|
+
const merged = existing.filter((t) => t.topicName !== input2.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
|
|
9968
10622
|
merged.push({
|
|
9969
10623
|
topicId: match.topic_id,
|
|
9970
10624
|
topicName: match.topic_name,
|
|
9971
|
-
action:
|
|
10625
|
+
action: input2.intent
|
|
9972
10626
|
});
|
|
9973
|
-
const guardrailAction =
|
|
9974
|
-
await mgmt.assignTopicsToProfile(
|
|
10627
|
+
const guardrailAction = input2.intent === "block" ? "allow" : "block";
|
|
10628
|
+
await mgmt.assignTopicsToProfile(input2.profileName, merged, guardrailAction);
|
|
9975
10629
|
return {
|
|
9976
10630
|
topicId: match.topic_id,
|
|
9977
10631
|
topicName: match.topic_name,
|
|
9978
|
-
profileName:
|
|
9979
|
-
intent:
|
|
10632
|
+
profileName: input2.profileName,
|
|
10633
|
+
intent: input2.intent
|
|
9980
10634
|
};
|
|
9981
10635
|
}
|
|
9982
10636
|
function registerApplyCommand(parent) {
|
|
@@ -10021,21 +10675,21 @@ function registerApplyCommand(parent) {
|
|
|
10021
10675
|
}
|
|
10022
10676
|
|
|
10023
10677
|
// src/cli/commands/topics-create.ts
|
|
10024
|
-
async function createOrUpdateTopic(mgmt,
|
|
10025
|
-
const topic = { name:
|
|
10678
|
+
async function createOrUpdateTopic(mgmt, input2) {
|
|
10679
|
+
const topic = { name: input2.name, description: input2.description, examples: input2.examples };
|
|
10026
10680
|
const errors = validateTopic(topic);
|
|
10027
10681
|
if (errors.length > 0) {
|
|
10028
10682
|
throw new Error(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
|
|
10029
10683
|
}
|
|
10030
10684
|
const existing = await mgmt.listTopics();
|
|
10031
|
-
const match = existing.find((t) => t.topic_name ===
|
|
10685
|
+
const match = existing.find((t) => t.topic_name === input2.name);
|
|
10032
10686
|
const request = {
|
|
10033
|
-
topic_name:
|
|
10034
|
-
description:
|
|
10035
|
-
examples:
|
|
10687
|
+
topic_name: input2.name,
|
|
10688
|
+
description: input2.description,
|
|
10689
|
+
examples: input2.examples
|
|
10036
10690
|
};
|
|
10037
10691
|
if (match && !match.topic_id) {
|
|
10038
|
-
throw new Error(`Existing topic '${
|
|
10692
|
+
throw new Error(`Existing topic '${input2.name}' has no topic_id`);
|
|
10039
10693
|
}
|
|
10040
10694
|
const result = match ? await mgmt.updateTopic(match.topic_id, request) : await mgmt.createTopic(request);
|
|
10041
10695
|
if (!result.topic_id) {
|
|
@@ -10093,7 +10747,7 @@ function registerCreateCommand(parent) {
|
|
|
10093
10747
|
}
|
|
10094
10748
|
|
|
10095
10749
|
// src/cli/commands/topics-eval.ts
|
|
10096
|
-
import { readFile as
|
|
10750
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
10097
10751
|
|
|
10098
10752
|
// src/core/prompt-loader.ts
|
|
10099
10753
|
function parseCsvLine(line) {
|
|
@@ -10236,7 +10890,7 @@ function registerEvalCommand(parent) {
|
|
|
10236
10890
|
resolveDeprecatedAliases(cmd, opts);
|
|
10237
10891
|
try {
|
|
10238
10892
|
const config = await loadConfig();
|
|
10239
|
-
const csvContent = await
|
|
10893
|
+
const csvContent = await readFile10(opts.prompts, "utf-8");
|
|
10240
10894
|
const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
|
|
10241
10895
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
10242
10896
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
@@ -10260,8 +10914,8 @@ function registerEvalCommand(parent) {
|
|
|
10260
10914
|
|
|
10261
10915
|
// src/cli/commands/topics-revert.ts
|
|
10262
10916
|
async function revertTopic(mgmt, profileName, topicName) {
|
|
10263
|
-
const
|
|
10264
|
-
const match =
|
|
10917
|
+
const topics2 = await mgmt.listTopics();
|
|
10918
|
+
const match = topics2.find((t) => t.topic_name === topicName);
|
|
10265
10919
|
if (!match || !match.topic_id) {
|
|
10266
10920
|
throw new Error(`Topic "${topicName}" not found`);
|
|
10267
10921
|
}
|
|
@@ -10408,7 +11062,7 @@ function completedBulkResults(state) {
|
|
|
10408
11062
|
}
|
|
10409
11063
|
async function writeBulkResults(outputPath, results) {
|
|
10410
11064
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
10411
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
11065
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID9()}`;
|
|
10412
11066
|
try {
|
|
10413
11067
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
10414
11068
|
encoding: "utf-8",
|
|
@@ -10535,7 +11189,7 @@ function registerRuntimeCommand(program) {
|
|
|
10535
11189
|
if (!config.airsApiKey && !config.airsApiToken) {
|
|
10536
11190
|
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
10537
11191
|
}
|
|
10538
|
-
const raw = await
|
|
11192
|
+
const raw = await readFile11(opts.file, "utf-8");
|
|
10539
11193
|
const prompts = parseInputFile(raw, opts.file);
|
|
10540
11194
|
if (prompts.length === 0) {
|
|
10541
11195
|
usageError("No prompts found in input file");
|
|
@@ -10750,16 +11404,17 @@ function registerRuntimeCommand(program) {
|
|
|
10750
11404
|
const fmt = await resolveOutput(deploymentProfilesList, opts);
|
|
10751
11405
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
10752
11406
|
const service = await createMgmtService();
|
|
10753
|
-
const
|
|
11407
|
+
const profiles3 = await service.listDeploymentProfiles({
|
|
10754
11408
|
unactivated: opts.unactivated
|
|
10755
11409
|
});
|
|
10756
|
-
renderDeploymentProfileList(
|
|
11410
|
+
renderDeploymentProfileList(profiles3, fmt);
|
|
10757
11411
|
} catch (err) {
|
|
10758
11412
|
fail(err);
|
|
10759
11413
|
}
|
|
10760
11414
|
});
|
|
10761
|
-
const
|
|
10762
|
-
|
|
11415
|
+
const profiles2 = runtime.command("profiles").description("Manage AIRS security profiles");
|
|
11416
|
+
registerProfileTransferCommands(profiles2);
|
|
11417
|
+
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
11418
|
"after",
|
|
10764
11419
|
examples(
|
|
10765
11420
|
"airs runtime profiles list",
|
|
@@ -10798,7 +11453,7 @@ function registerRuntimeCommand(program) {
|
|
|
10798
11453
|
fail(err);
|
|
10799
11454
|
}
|
|
10800
11455
|
});
|
|
10801
|
-
const profilesGet =
|
|
11456
|
+
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
11457
|
try {
|
|
10803
11458
|
const fmt = await resolveOutput(profilesGet, opts);
|
|
10804
11459
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
@@ -10807,10 +11462,10 @@ function registerRuntimeCommand(program) {
|
|
|
10807
11462
|
nameOrId
|
|
10808
11463
|
);
|
|
10809
11464
|
if (opts.revision !== void 0 || opts.allVersions) {
|
|
10810
|
-
const
|
|
11465
|
+
const profiles3 = (await service.listAllProfiles({ latest: false })).filter(
|
|
10811
11466
|
(profile) => isUuid ? profile.profileId === nameOrId : profile.profileName === nameOrId
|
|
10812
11467
|
);
|
|
10813
|
-
const selected = opts.revision === void 0 ?
|
|
11468
|
+
const selected = opts.revision === void 0 ? profiles3 : profiles3.filter((profile) => profile.revision === opts.revision);
|
|
10814
11469
|
if (selected.length === 0) throw new Error(`Profile ${nameOrId} not found`);
|
|
10815
11470
|
if (opts.allVersions) emitList(profilesView, selected, fmt);
|
|
10816
11471
|
else emitDetail(profilesView, selected[0], fmt);
|
|
@@ -10822,7 +11477,7 @@ function registerRuntimeCommand(program) {
|
|
|
10822
11477
|
fail(err);
|
|
10823
11478
|
}
|
|
10824
11479
|
});
|
|
10825
|
-
|
|
11480
|
+
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
11481
|
const service = await createMgmtService();
|
|
10827
11482
|
try {
|
|
10828
11483
|
renderRuntimeConfigHeader();
|
|
@@ -10877,7 +11532,7 @@ function registerRuntimeCommand(program) {
|
|
|
10877
11532
|
fail(err);
|
|
10878
11533
|
}
|
|
10879
11534
|
});
|
|
10880
|
-
|
|
11535
|
+
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
11536
|
try {
|
|
10882
11537
|
renderRuntimeConfigHeader();
|
|
10883
11538
|
const service = await createMgmtService();
|
|
@@ -10926,7 +11581,7 @@ function registerRuntimeCommand(program) {
|
|
|
10926
11581
|
fail(err);
|
|
10927
11582
|
}
|
|
10928
11583
|
});
|
|
10929
|
-
|
|
11584
|
+
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
11585
|
try {
|
|
10931
11586
|
renderRuntimeConfigHeader();
|
|
10932
11587
|
const service = await createMgmtService();
|
|
@@ -10959,7 +11614,7 @@ function registerRuntimeCommand(program) {
|
|
|
10959
11614
|
fail(err);
|
|
10960
11615
|
}
|
|
10961
11616
|
});
|
|
10962
|
-
registerCleanupCommand(
|
|
11617
|
+
registerCleanupCommand(profiles2);
|
|
10963
11618
|
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
11619
|
registerDeprecatedAlias(resumePoll, {
|
|
10965
11620
|
oldFlag: "--output <file>",
|
|
@@ -11107,10 +11762,10 @@ function registerRuntimeCommand(program) {
|
|
|
11107
11762
|
);
|
|
11108
11763
|
process.exitCode = 1;
|
|
11109
11764
|
});
|
|
11110
|
-
const
|
|
11111
|
-
registerApplyCommand(
|
|
11112
|
-
registerCreateCommand(
|
|
11113
|
-
|
|
11765
|
+
const topics2 = runtime.command("topics").description("Manage AIRS custom topics and guardrail generation");
|
|
11766
|
+
registerApplyCommand(topics2);
|
|
11767
|
+
registerCreateCommand(topics2);
|
|
11768
|
+
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
11769
|
try {
|
|
11115
11770
|
renderRuntimeConfigHeader();
|
|
11116
11771
|
const service = await createMgmtService();
|
|
@@ -11128,8 +11783,8 @@ function registerRuntimeCommand(program) {
|
|
|
11128
11783
|
fail(err);
|
|
11129
11784
|
}
|
|
11130
11785
|
});
|
|
11131
|
-
registerEvalCommand(
|
|
11132
|
-
const topicsGet =
|
|
11786
|
+
registerEvalCommand(topics2);
|
|
11787
|
+
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
11788
|
try {
|
|
11134
11789
|
const fmt = await resolveOutput(topicsGet, opts);
|
|
11135
11790
|
if (fmt === "pretty") renderRuntimeConfigHeader();
|
|
@@ -11138,10 +11793,10 @@ function registerRuntimeCommand(program) {
|
|
|
11138
11793
|
nameOrId
|
|
11139
11794
|
);
|
|
11140
11795
|
if (opts.revision !== void 0 || opts.allVersions) {
|
|
11141
|
-
const
|
|
11796
|
+
const topics3 = (await service.listTopics()).filter(
|
|
11142
11797
|
(topic) => isUuid ? topic.topic_id === nameOrId : topic.topic_name === nameOrId
|
|
11143
11798
|
);
|
|
11144
|
-
const selected = opts.revision === void 0 ?
|
|
11799
|
+
const selected = opts.revision === void 0 ? topics3 : topics3.filter((topic) => topic.revision === opts.revision);
|
|
11145
11800
|
if (selected.length === 0) throw new Error(`Topic ${nameOrId} not found`);
|
|
11146
11801
|
if (opts.allVersions) emitList(topicsView, selected, fmt);
|
|
11147
11802
|
else emitDetail(topicsView, selected[0], fmt);
|
|
@@ -11153,7 +11808,7 @@ function registerRuntimeCommand(program) {
|
|
|
11153
11808
|
fail(err);
|
|
11154
11809
|
}
|
|
11155
11810
|
});
|
|
11156
|
-
const topicsList = registerListFlags(
|
|
11811
|
+
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
11812
|
try {
|
|
11158
11813
|
const fmt = await resolveOutput(topicsList, opts);
|
|
11159
11814
|
const params = resolveListParams(topicsList, opts, { dialect: "offset" });
|
|
@@ -11174,9 +11829,9 @@ function registerRuntimeCommand(program) {
|
|
|
11174
11829
|
fail(err);
|
|
11175
11830
|
}
|
|
11176
11831
|
});
|
|
11177
|
-
registerRevertCommand(
|
|
11178
|
-
registerSampleCommand(
|
|
11179
|
-
|
|
11832
|
+
registerRevertCommand(topics2);
|
|
11833
|
+
registerSampleCommand(topics2);
|
|
11834
|
+
topics2.command("update <topicId>").description("Update a custom topic").requiredOption("--config <path>", "JSON file with topic updates").action(async (topicId, opts) => {
|
|
11180
11835
|
try {
|
|
11181
11836
|
renderRuntimeConfigHeader();
|
|
11182
11837
|
const service = await createMgmtService();
|
|
@@ -11191,6 +11846,281 @@ function registerRuntimeCommand(program) {
|
|
|
11191
11846
|
registerDlpCommands(runtime);
|
|
11192
11847
|
}
|
|
11193
11848
|
|
|
11849
|
+
// src/config/tenant-settings.ts
|
|
11850
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
11851
|
+
import { constants } from "fs";
|
|
11852
|
+
import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
11853
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
11854
|
+
var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
11855
|
+
function validateTenantSettingKey(key) {
|
|
11856
|
+
if (!Object.hasOwn(ConfigSchema.shape, key))
|
|
11857
|
+
throw new Error(`Unknown configuration key. Supported keys: ${TENANT_CONFIG_KEYS.join(", ")}`);
|
|
11858
|
+
}
|
|
11859
|
+
function isTenantSecret(key) {
|
|
11860
|
+
return /(?:key|secret|token|password)$/i.test(key);
|
|
11861
|
+
}
|
|
11862
|
+
function validateCredentials(config) {
|
|
11863
|
+
for (const key of ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"]) {
|
|
11864
|
+
if (typeof config[key] !== "string" || !config[key].trim())
|
|
11865
|
+
throw new Error(
|
|
11866
|
+
"Tenant config requires nonempty mgmtTsgId, mgmtClientId and mgmtClientSecret"
|
|
11867
|
+
);
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11870
|
+
async function createManagedTenant(name, config) {
|
|
11871
|
+
validateTenantName(name);
|
|
11872
|
+
for (const key of Object.keys(config)) validateTenantSettingKey(key);
|
|
11873
|
+
const parsed = ConfigSchema.safeParse(config);
|
|
11874
|
+
if (!parsed.success) throw new Error("Invalid tenant configuration");
|
|
11875
|
+
validateCredentials(config);
|
|
11876
|
+
if (readTenantStore().tenants.some((entry) => entry.name === name))
|
|
11877
|
+
throw new Error("Tenant name already exists");
|
|
11878
|
+
const directory = join4(dirname4(tenantStorePath()), "configs");
|
|
11879
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
11880
|
+
const path3 = join4(directory, `${name}-${randomUUID10()}.json`);
|
|
11881
|
+
const file = await open2(path3, "wx", 384);
|
|
11882
|
+
try {
|
|
11883
|
+
const data = Object.fromEntries(
|
|
11884
|
+
Object.keys(config).map((key) => [key, Reflect.get(parsed.data, key)])
|
|
11885
|
+
);
|
|
11886
|
+
await file.writeFile(`${JSON.stringify(data, null, 2)}
|
|
11887
|
+
`);
|
|
11888
|
+
await file.sync();
|
|
11889
|
+
await file.close();
|
|
11890
|
+
return await createTenant(name, path3);
|
|
11891
|
+
} catch {
|
|
11892
|
+
await file.close();
|
|
11893
|
+
await unlink2(path3);
|
|
11894
|
+
throw new Error("Could not create tenant configuration; registration was not completed");
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11897
|
+
async function setTenantSetting(name, key, value) {
|
|
11898
|
+
validateTenantSettingKey(key);
|
|
11899
|
+
const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
|
|
11900
|
+
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
11901
|
+
if (key === "mgmtTsgId" && value !== entry.tsgId)
|
|
11902
|
+
throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
|
|
11903
|
+
const path3 = entry.configPath;
|
|
11904
|
+
const lockPath = `${path3}.lock`;
|
|
11905
|
+
const lock = await open2(lockPath, "wx", 384).catch(() => {
|
|
11906
|
+
throw new Error(
|
|
11907
|
+
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
11908
|
+
);
|
|
11909
|
+
});
|
|
11910
|
+
const temporary = join4(dirname4(path3), `.airs-config-${randomUUID10()}.tmp`);
|
|
11911
|
+
let created = false;
|
|
11912
|
+
try {
|
|
11913
|
+
const stat2 = await lstat6(path3);
|
|
11914
|
+
if (!stat2.isFile() || (stat2.mode & 146) === 0)
|
|
11915
|
+
throw new Error("Tenant config is not a writable regular file");
|
|
11916
|
+
await access(path3, constants.W_OK);
|
|
11917
|
+
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
11918
|
+
const result = ConfigSchema.safeParse({ ...current, [key]: value });
|
|
11919
|
+
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
11920
|
+
const next = { ...current, [key]: Reflect.get(result.data, key) };
|
|
11921
|
+
validateCredentials(next);
|
|
11922
|
+
const file = await open2(temporary, "wx", 384);
|
|
11923
|
+
created = true;
|
|
11924
|
+
try {
|
|
11925
|
+
await file.writeFile(`${JSON.stringify(next, null, 2)}
|
|
11926
|
+
`);
|
|
11927
|
+
await file.sync();
|
|
11928
|
+
} finally {
|
|
11929
|
+
await file.close();
|
|
11930
|
+
}
|
|
11931
|
+
await rename2(temporary, path3);
|
|
11932
|
+
created = false;
|
|
11933
|
+
} finally {
|
|
11934
|
+
if (created) await unlink2(temporary).catch(() => {
|
|
11935
|
+
});
|
|
11936
|
+
await lock.close();
|
|
11937
|
+
await unlink2(lockPath);
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
|
|
11941
|
+
// src/cli/tenant-input.ts
|
|
11942
|
+
import { input, password } from "@inquirer/prompts";
|
|
11943
|
+
async function promptTenantValue(message, secret = false) {
|
|
11944
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
11945
|
+
throw new Error(
|
|
11946
|
+
"Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
|
|
11947
|
+
);
|
|
11948
|
+
const prompt = secret ? password : input;
|
|
11949
|
+
return prompt(
|
|
11950
|
+
{
|
|
11951
|
+
message,
|
|
11952
|
+
validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
|
|
11953
|
+
},
|
|
11954
|
+
{ input: process.stdin, output: process.stderr }
|
|
11955
|
+
);
|
|
11956
|
+
}
|
|
11957
|
+
async function readTenantStdin() {
|
|
11958
|
+
if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
|
|
11959
|
+
const chunks = [];
|
|
11960
|
+
let size = 0;
|
|
11961
|
+
for await (const chunk of process.stdin) {
|
|
11962
|
+
const buffer = Buffer.from(chunk);
|
|
11963
|
+
size += buffer.length;
|
|
11964
|
+
if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
|
|
11965
|
+
chunks.push(buffer);
|
|
11966
|
+
}
|
|
11967
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
11968
|
+
if (!value.trim() || /[\r\n\0]/.test(value))
|
|
11969
|
+
throw new Error("Provide one nonempty configuration value on stdin");
|
|
11970
|
+
return value;
|
|
11971
|
+
}
|
|
11972
|
+
|
|
11973
|
+
// src/cli/commands/tenant.ts
|
|
11974
|
+
function tenantInputFailure(error) {
|
|
11975
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
11976
|
+
ui.status("Cancelled; no configuration saved.");
|
|
11977
|
+
process.exitCode = 130;
|
|
11978
|
+
return;
|
|
11979
|
+
}
|
|
11980
|
+
fail(error);
|
|
11981
|
+
}
|
|
11982
|
+
var COLUMNS2 = [
|
|
11983
|
+
{ key: "name", label: "Tenant" },
|
|
11984
|
+
{ key: "active", label: "Selected" },
|
|
11985
|
+
{ key: "tsgId", label: "TSG ID" },
|
|
11986
|
+
{ key: "configPath", label: "Config file" }
|
|
11987
|
+
];
|
|
11988
|
+
function registerTenantCommand(program) {
|
|
11989
|
+
const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
|
|
11990
|
+
"after",
|
|
11991
|
+
examples(
|
|
11992
|
+
"airs tenant create development",
|
|
11993
|
+
"airs tenant set development defaultOutput yaml",
|
|
11994
|
+
"airs tenant create production --config /secure/production.json",
|
|
11995
|
+
"airs tenant switch production",
|
|
11996
|
+
"airs tenant read",
|
|
11997
|
+
"airs tenant switch default"
|
|
11998
|
+
)
|
|
11999
|
+
);
|
|
12000
|
+
tenant.command("create <name>").description(
|
|
12001
|
+
"Create a config with guided prompts, or register an existing JSON file; does not activate it"
|
|
12002
|
+
).option("--config <path>", "Existing Prisma AIRS JSON config file (no copying or editing)").option("--tsg-id <id>", "Tenant service group ID for a new config").option("--client-id <id>", "OAuth client ID for a new config").option("--client-secret-stdin", "Read the new config OAuth secret from piped stdin").action(
|
|
12003
|
+
async (name, opts) => {
|
|
12004
|
+
try {
|
|
12005
|
+
validateTenantName(name);
|
|
12006
|
+
if (opts.config && (opts.tsgId !== void 0 || opts.clientId !== void 0 || opts.clientSecretStdin))
|
|
12007
|
+
throw new Error("--config cannot be combined with new-config options");
|
|
12008
|
+
if (!opts.config && readTenantStore().tenants.some((entry2) => entry2.name === name))
|
|
12009
|
+
throw new Error("Tenant name already exists");
|
|
12010
|
+
const entry = opts.config ? await createTenant(name, opts.config) : await createManagedTenant(name, {
|
|
12011
|
+
mgmtTsgId: opts.tsgId ?? await promptTenantValue("Tenant service group ID (mgmtTsgId):"),
|
|
12012
|
+
mgmtClientId: opts.clientId ?? await promptTenantValue("OAuth client ID (mgmtClientId):"),
|
|
12013
|
+
mgmtClientSecret: opts.clientSecretStdin ? await readTenantStdin() : await promptTenantValue("OAuth client secret (mgmtClientSecret):", true)
|
|
12014
|
+
});
|
|
12015
|
+
ui.success(
|
|
12016
|
+
`Registered ${entry.name} (TSG ${entry.tsgId}); ${opts.config ? "config file unchanged" : "private config created"}. Use airs tenant switch ${entry.name}.`
|
|
12017
|
+
);
|
|
12018
|
+
} catch (error) {
|
|
12019
|
+
tenantInputFailure(error);
|
|
12020
|
+
}
|
|
12021
|
+
}
|
|
12022
|
+
);
|
|
12023
|
+
tenant.command("set <name> <key> [value]").description("Update one named tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
|
|
12024
|
+
async (name, key, value, opts) => {
|
|
12025
|
+
try {
|
|
12026
|
+
validateTenantSettingKey(key);
|
|
12027
|
+
if (!readTenantStore().tenants.some((entry) => entry.name === name))
|
|
12028
|
+
throw new Error("Tenant not found; register a named tenant first");
|
|
12029
|
+
if (opts.stdin && value !== void 0)
|
|
12030
|
+
throw new Error("Use a value or --stdin, not both");
|
|
12031
|
+
if (isTenantSecret(key) && value !== void 0)
|
|
12032
|
+
throw new Error(
|
|
12033
|
+
"Do not pass credentials as arguments. Omit the value for a hidden prompt, or use --stdin."
|
|
12034
|
+
);
|
|
12035
|
+
const setting = opts.stdin ? await readTenantStdin() : value ?? await promptTenantValue(`${key}:`, isTenantSecret(key));
|
|
12036
|
+
await setTenantSetting(name, key, setting);
|
|
12037
|
+
ui.success(`Updated ${key} for tenant ${name}; selection unchanged.`);
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
tenantInputFailure(error);
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
);
|
|
12043
|
+
tenant.command("switch <name>").description(
|
|
12044
|
+
"Persist the selected tenant; default restores legacy config/environment resolution"
|
|
12045
|
+
).action(async (name) => {
|
|
12046
|
+
try {
|
|
12047
|
+
const entry = await switchTenant(name);
|
|
12048
|
+
ui.success(
|
|
12049
|
+
entry ? `Selected ${entry.name} (TSG ${entry.tsgId})` : "Selected default; explicit config/environment overrides still apply"
|
|
12050
|
+
);
|
|
12051
|
+
} catch (error) {
|
|
12052
|
+
fail(error);
|
|
12053
|
+
}
|
|
12054
|
+
});
|
|
12055
|
+
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) => {
|
|
12056
|
+
try {
|
|
12057
|
+
const format = await resolveOutput(list, opts, { ignoreConfig: true });
|
|
12058
|
+
const store = readTenantStore();
|
|
12059
|
+
const explicit = process.env.PRISMA_AIRS_CONFIG_PATH;
|
|
12060
|
+
const rows = [
|
|
12061
|
+
{
|
|
12062
|
+
name: "default",
|
|
12063
|
+
active: store.active === null,
|
|
12064
|
+
tsgId: "",
|
|
12065
|
+
configPath: defaultTenantConfigPath()
|
|
12066
|
+
},
|
|
12067
|
+
...store.tenants.map((entry) => ({ ...entry, active: entry.name === store.active }))
|
|
12068
|
+
];
|
|
12069
|
+
if (explicit)
|
|
12070
|
+
ui.status("PRISMA_AIRS_CONFIG_PATH overrides the selected tenant for API commands.");
|
|
12071
|
+
console.log(formatOutput(rows, COLUMNS2, format === "pretty" ? "table" : format));
|
|
12072
|
+
} catch (error) {
|
|
12073
|
+
fail(error);
|
|
12074
|
+
}
|
|
12075
|
+
});
|
|
12076
|
+
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) => {
|
|
12077
|
+
try {
|
|
12078
|
+
const format = await resolveOutput(read, opts, { ignoreConfig: true });
|
|
12079
|
+
const store = readTenantStore();
|
|
12080
|
+
const selected = name ?? store.active ?? "default";
|
|
12081
|
+
const entry = store.tenants.find((value) => value.name === selected);
|
|
12082
|
+
if (selected !== "default" && !entry) throw new Error("Tenant not found");
|
|
12083
|
+
const path3 = entry?.configPath ?? expandConfigPath(process.env.PRISMA_AIRS_CONFIG_PATH || defaultTenantConfigPath());
|
|
12084
|
+
const config = readTenantConfig(path3, entry?.tsgId);
|
|
12085
|
+
const rows = Object.entries(config).map(([key, value]) => ({
|
|
12086
|
+
key,
|
|
12087
|
+
value: /key|secret|token|password/i.test(key) && value ? "[REDACTED]" : value ?? ""
|
|
12088
|
+
}));
|
|
12089
|
+
ui.status(`Tenant ${selected}${entry ? ` (TSG ${entry.tsgId})` : ""}: ${path3}`);
|
|
12090
|
+
console.log(
|
|
12091
|
+
formatOutput(
|
|
12092
|
+
rows,
|
|
12093
|
+
[
|
|
12094
|
+
{ key: "key", label: "Key" },
|
|
12095
|
+
{ key: "value", label: "Value" }
|
|
12096
|
+
],
|
|
12097
|
+
format === "pretty" ? "table" : format
|
|
12098
|
+
)
|
|
12099
|
+
);
|
|
12100
|
+
} catch (error) {
|
|
12101
|
+
fail(error);
|
|
12102
|
+
}
|
|
12103
|
+
});
|
|
12104
|
+
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) => {
|
|
12105
|
+
try {
|
|
12106
|
+
const store = readTenantStore();
|
|
12107
|
+
if (name === "default" || name === store.active)
|
|
12108
|
+
throw new Error("Switch away first; default and active tenants cannot be deleted");
|
|
12109
|
+
if (!store.tenants.some((entry) => entry.name === name))
|
|
12110
|
+
throw new Error("Tenant not found");
|
|
12111
|
+
await confirmOrAbort(
|
|
12112
|
+
`Unregister tenant ${name}? Its config file will be retained.`,
|
|
12113
|
+
Boolean(opts.force),
|
|
12114
|
+
{ action: `unregister tenant ${name}` }
|
|
12115
|
+
);
|
|
12116
|
+
await deleteTenant(name);
|
|
12117
|
+
ui.success(`Unregistered ${name}; source config file retained`);
|
|
12118
|
+
} catch (error) {
|
|
12119
|
+
fail(error);
|
|
12120
|
+
}
|
|
12121
|
+
});
|
|
12122
|
+
}
|
|
12123
|
+
|
|
11194
12124
|
// src/cli/program.ts
|
|
11195
12125
|
var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
11196
12126
|
"categories",
|
|
@@ -11245,8 +12175,8 @@ function applySortedHelp(cmd) {
|
|
|
11245
12175
|
for (const sub of cmd.commands) applySortedHelp(sub);
|
|
11246
12176
|
}
|
|
11247
12177
|
function buildProgram() {
|
|
11248
|
-
const here =
|
|
11249
|
-
const pkg = JSON.parse(readFileSync4(
|
|
12178
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
12179
|
+
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
11250
12180
|
const program = new Command();
|
|
11251
12181
|
program.name("airs").description(
|
|
11252
12182
|
"CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
|
|
@@ -11254,6 +12184,11 @@ function buildProgram() {
|
|
|
11254
12184
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
11255
12185
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
11256
12186
|
setQuiet(Boolean(root.quiet));
|
|
12187
|
+
const profileTransfer = actionCommand.parent?.name() === "profiles" && actionCommand.parent.parent?.name() === "runtime" && ["backup", "restore"].includes(actionCommand.name());
|
|
12188
|
+
if (profileTransfer && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
12189
|
+
usageError(
|
|
12190
|
+
"Disable --debug and PANW_AI_SEC_DEBUG for profile transfer; backups can contain sensitive policy configuration"
|
|
12191
|
+
);
|
|
11257
12192
|
const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway", "agentguard"].includes(
|
|
11258
12193
|
actionCommand.parent?.name() ?? ""
|
|
11259
12194
|
) && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
|
|
@@ -11261,7 +12196,7 @@ function buildProgram() {
|
|
|
11261
12196
|
usageError(
|
|
11262
12197
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
11263
12198
|
);
|
|
11264
|
-
if (!isEnvironmentReport && !(actionCommand.name() === "report" && actionCommand.parent?.name() === "redteam") && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
12199
|
+
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
12200
|
try {
|
|
11266
12201
|
const format = await resolveOutput(actionCommand, actionCommand.opts());
|
|
11267
12202
|
actionCommand.setOptionValueWithSource("output", format, "implied");
|
|
@@ -11270,9 +12205,9 @@ function buildProgram() {
|
|
|
11270
12205
|
}
|
|
11271
12206
|
}
|
|
11272
12207
|
if (root.debug) {
|
|
11273
|
-
const logPath =
|
|
12208
|
+
const logPath = join5(
|
|
11274
12209
|
process.cwd(),
|
|
11275
|
-
`debug-api-${Date.now()}-${
|
|
12210
|
+
`debug-api-${Date.now()}-${randomUUID11().slice(0, 8)}.jsonl`
|
|
11276
12211
|
);
|
|
11277
12212
|
try {
|
|
11278
12213
|
let ancestor = actionCommand;
|
|
@@ -11297,6 +12232,7 @@ function buildProgram() {
|
|
|
11297
12232
|
registerAgentGuardCommand(program);
|
|
11298
12233
|
registerAiGatewayCommand(program);
|
|
11299
12234
|
registerConfigCommand(program);
|
|
12235
|
+
registerTenantCommand(program);
|
|
11300
12236
|
registerDoctorCommand(program);
|
|
11301
12237
|
registerCompletionCommand(program);
|
|
11302
12238
|
applyListDeleteAliases(program);
|