@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/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-36Z6IPH6.js";
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 randomUUID9 } from "crypto";
62
+ import { randomUUID as randomUUID11 } from "crypto";
53
63
  import { readFileSync as readFileSync4 } from "fs";
54
- import { dirname as dirname4, join as join4 } from "path";
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 maxPages = options.maxPages ?? 40;
136
- if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100)
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 < maxPages; 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 ${maxPages}-page budget.`);
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(profiles, format = "pretty") {
2598
- if (profiles.length === 0) {
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 = profiles.map((p) => ({
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 profiles) {
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(topics, format = "pretty") {
2708
- if (topics.length === 0) {
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 = topics.map((t) => ({
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 topics) {
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(profiles, format = "pretty") {
2937
- if (profiles.length === 0) {
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 = profiles.map((p) => ({
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 profiles) {
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 failure;
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((resolve7, reject) => {
3511
- process.stdout.write(text2, (error) => error ? reject(error) : resolve7());
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
- failure = error;
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 && failure?.code !== "EPIPE") {
3555
- if (failure?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
3556
- usageError(failure instanceof Error ? failure.message : "Invalid inference options");
3557
- fail(failure);
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(input, init2) {
3681
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
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 ?? (input instanceof Request ? input.headers : void 0)
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(input, init2);
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 ?? (input instanceof Request ? input.method : "GET");
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(input, init2);
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, load2) {
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 load2(client)), format, label);
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, load2) {
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 load2(client), format);
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 maxPages = Number(opts.maxPages);
5234
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
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((resolve7) => {
5890
- timer = setTimeout(() => resolve7(TIMED_OUT), ms);
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 topics = await service.listTopics();
6056
- return topics.length;
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((resolve7, reject) => {
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 resolve7();
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 maxPages = Number(opts.maxPages);
6791
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
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(input) {
6952
- const trimmed = input.trim();
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(input, flag) {
6975
- const n = Number.parseInt(input, 10);
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 "${input}"`);
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(input) {
7073
+ function parseAdapterVariables(input2) {
7064
7074
  let parsed;
7065
7075
  try {
7066
- parsed = JSON.parse(input);
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 randomUUID8 } from "crypto";
8084
+ import { randomUUID as randomUUID9 } from "crypto";
8075
8085
  import * as fs5 from "fs";
8076
- import { readFile as readFile10 } from "fs/promises";
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 profiles = parseList(flags.dlpProfiles);
8141
- if (profiles) {
8142
- dld.member = profiles.map((text2) => ({ text: text2 }));
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(profiles) {
10184
+ function findDuplicateProfiles(profiles2) {
9531
10185
  const groups = /* @__PURE__ */ new Map();
9532
- for (const p of profiles) {
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 resolve5 } from "path";
9632
- import { dump as dump6 } from "js-yaml";
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 = resolve5(text(opts.outputFile, "--output-file"));
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" ? dump6(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
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(resolve5(String(opts.outputFile)), `${output}
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 resolve6 } from "path";
9896
- import { randomUUID as randomUUID7 } from "crypto";
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 maxPages = Number(opts.maxPages);
9914
- if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
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(":", "-")}-${randomUUID7().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
9921
- const outputPath = opts.outputFile === "-" ? void 0 : resolve6(opts.outputFile ?? defaultName);
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, input) {
10614
+ async function applyTopicToProfile(mgmt, input2) {
9961
10615
  const allTopics = await mgmt.listTopics();
9962
- const match = allTopics.find((t) => t.topic_name === input.topicName);
10616
+ const match = allTopics.find((t) => t.topic_name === input2.topicName);
9963
10617
  if (!match || !match.topic_id) {
9964
- throw new Error(`Topic "${input.topicName}" not found. Create it first with "topics create".`);
10618
+ throw new Error(`Topic "${input2.topicName}" not found. Create it first with "topics create".`);
9965
10619
  }
9966
- const existing = await mgmt.getProfileTopics(input.profileName);
9967
- const merged = existing.filter((t) => t.topicName !== input.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
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: input.intent
10625
+ action: input2.intent
9972
10626
  });
9973
- const guardrailAction = input.intent === "block" ? "allow" : "block";
9974
- await mgmt.assignTopicsToProfile(input.profileName, merged, guardrailAction);
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: input.profileName,
9979
- intent: input.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, input) {
10025
- const topic = { name: input.name, description: input.description, examples: input.examples };
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 === input.name);
10685
+ const match = existing.find((t) => t.topic_name === input2.name);
10032
10686
  const request = {
10033
- topic_name: input.name,
10034
- description: input.description,
10035
- examples: input.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 '${input.name}' has no topic_id`);
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 readFile9 } from "fs/promises";
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 readFile9(opts.prompts, "utf-8");
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 topics = await mgmt.listTopics();
10264
- const match = topics.find((t) => t.topic_name === topicName);
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}-${randomUUID8()}`;
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 readFile10(opts.file, "utf-8");
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 profiles2 = await service.listDeploymentProfiles({
11407
+ const profiles3 = await service.listDeploymentProfiles({
10754
11408
  unactivated: opts.unactivated
10755
11409
  });
10756
- renderDeploymentProfileList(profiles2, fmt);
11410
+ renderDeploymentProfileList(profiles3, fmt);
10757
11411
  } catch (err) {
10758
11412
  fail(err);
10759
11413
  }
10760
11414
  });
10761
- const profiles = runtime.command("profiles").description("Manage AIRS security profiles");
10762
- const profilesList = registerListFlags(profiles.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(
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 = profiles.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) => {
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 profiles2 = (await service.listAllProfiles({ latest: false })).filter(
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 ? profiles2 : profiles2.filter((profile) => profile.revision === opts.revision);
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
- profiles.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) => {
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
- profiles.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) => {
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
- profiles.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) => {
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(profiles);
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 topics = runtime.command("topics").description("Manage AIRS custom topics and guardrail generation");
11111
- registerApplyCommand(topics);
11112
- registerCreateCommand(topics);
11113
- topics.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) => {
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(topics);
11132
- const topicsGet = topics.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) => {
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 topics2 = (await service.listTopics()).filter(
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 ? topics2 : topics2.filter((topic) => topic.revision === opts.revision);
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(topics.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) => {
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(topics);
11178
- registerSampleCommand(topics);
11179
- topics.command("update <topicId>").description("Update a custom topic").requiredOption("--config <path>", "JSON file with topic updates").action(async (topicId, opts) => {
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 = dirname4(fileURLToPath(import.meta.url));
11249
- const pkg = JSON.parse(readFileSync4(join4(here, "../../package.json"), "utf-8"));
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 = join4(
12208
+ const logPath = join5(
11274
12209
  process.cwd(),
11275
- `debug-api-${Date.now()}-${randomUUID9().slice(0, 8)}.jsonl`
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);