@cdot65/prisma-airs-cli 5.4.1 → 5.5.0

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