@cdot65/prisma-airs-cli 5.3.0 → 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-7Z76VCB3.js";
40
+ } from "../chunk-NKAVUBKI.js";
34
41
 
35
42
  // src/cli/index.ts
36
43
  import "dotenv/config";
@@ -49,189 +56,36 @@ function installProcessGuards() {
49
56
  }
50
57
 
51
58
  // src/cli/program.ts
52
- import { randomUUID as randomUUID8 } 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";
56
63
  import { Command } from "commander";
57
64
 
58
- // src/cli/commands/aigateway.ts
65
+ // src/cli/commands/agentguard.ts
66
+ import { randomUUID } from "crypto";
67
+ import { lstat } from "fs/promises";
68
+ import { resolve } from "path";
59
69
  import {
60
- GatewayDefaultsInputSchema,
61
- GatewayRateLimitInputSchema,
62
- GatewayUsageLimitInputSchema
70
+ AgentGuardClient,
71
+ AgentGuardPageOptionsSchema,
72
+ AgentGuardScanListOptionsSchema,
73
+ AgentGuardStatsOptionsSchema
63
74
  } from "@cdot65/prisma-airs-sdk";
75
+ import { dump as dump3 } from "js-yaml";
76
+ import { z } from "zod";
64
77
 
65
- // src/airs/aigateway.ts
66
- import {
67
- AIGatewayChartFiltersSchema,
68
- AIGatewayClient
69
- } from "@cdot65/prisma-airs-sdk";
70
- function toLimitArray(value) {
71
- if (Array.isArray(value)) return value;
72
- if (value !== null && typeof value === "object") return [value];
73
- return [];
74
- }
75
- function normalizeWorkspace(raw) {
76
- return {
77
- id: raw.id,
78
- slug: raw.slug,
79
- name: raw.name,
80
- icon: raw.icon,
81
- description: raw.description,
82
- createdAt: raw.created_at,
83
- lastUpdatedAt: raw.last_updated_at,
84
- isDefault: Boolean(raw.is_default),
85
- status: raw.status,
86
- scopeName: raw.scope_name
87
- };
88
- }
89
- function normalizeWorkspaceDetail(raw) {
78
+ // src/config/client-options.ts
79
+ function agentGuardClientOptions(config) {
90
80
  return {
91
- ...normalizeWorkspace(raw),
92
- defaults: raw.defaults,
93
- usageLimits: toLimitArray(raw.usage_limits),
94
- rateLimits: toLimitArray(raw.rate_limits),
95
- securitySettings: raw.security_settings,
96
- dataPlaneSecuritySettings: raw.data_plane_security_settings,
97
- settings: raw.settings
81
+ clientId: config.mgmtClientId,
82
+ clientSecret: config.mgmtClientSecret,
83
+ tsgId: config.mgmtTsgId,
84
+ dataEndpoint: config.agentGuardDataEndpoint,
85
+ mgmtEndpoint: config.agentGuardMgmtEndpoint,
86
+ tokenEndpoint: config.agentGuardTokenEndpoint ?? config.mgmtTokenEndpoint
98
87
  };
99
88
  }
100
- function aiGatewayGrantHint(err) {
101
- const status = err?.status ?? err?.statusCode;
102
- if (status !== 403) return void 0;
103
- const message = err instanceof Error ? err.message : String(err);
104
- const grant = message.includes("AB03") ? "the service account is missing a workspace-scope grant (data plane, /ai_gw/v2)" : "the service account is missing a tenant-root admin grant (admin plane, /ai_gw/admin/v2)";
105
- return `${grant}. SCM Access Management edits the existing role row by default \u2014 use "Add Role" so the account ends up with both role rows, not one row moved.`;
106
- }
107
- var SdkAiGatewayService = class {
108
- client;
109
- constructor(opts) {
110
- this.client = new AIGatewayClient(opts);
111
- }
112
- async listWorkspaces(options) {
113
- const response = await this.client.workspaces.list(options);
114
- return response.data.map(normalizeWorkspace);
115
- }
116
- async listAllWorkspaces() {
117
- const [active, archived] = await Promise.all([
118
- this.client.workspaces.list({ plane: "admin" }),
119
- this.client.workspaces.list({ plane: "admin", status: "archived" })
120
- ]);
121
- return [
122
- ...active.data,
123
- ...archived.data
124
- ].map(normalizeWorkspace);
125
- }
126
- async getWorkspace(workspaceRef, options) {
127
- try {
128
- const raw = await this.client.workspaces.get(workspaceRef, options);
129
- return normalizeWorkspaceDetail(raw);
130
- } catch (err) {
131
- const status = err.statusCode;
132
- if (status !== 404) throw err;
133
- const resolved = await this.resolveWorkspaceRef(workspaceRef, [
134
- options?.plane ?? "data",
135
- "admin"
136
- ]);
137
- if (resolved === workspaceRef) throw err;
138
- const raw = await this.client.workspaces.get(resolved, options);
139
- return normalizeWorkspaceDetail(raw);
140
- }
141
- }
142
- async createWorkspace(request) {
143
- const body = {
144
- name: request.name,
145
- scope_name: request.scopeName
146
- };
147
- if (request.description !== void 0) body.description = request.description;
148
- if (request.icon !== void 0) body.icon = request.icon;
149
- if (request.defaults !== void 0) body.defaults = request.defaults;
150
- if (request.users !== void 0) body.users = request.users;
151
- if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
152
- if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
153
- const created = await this.client.workspaces.create(body);
154
- return this.refetchAfterWrite(created.id, created);
155
- }
156
- async updateWorkspace(workspaceRef, request) {
157
- const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
158
- const body = {};
159
- if (request.name !== void 0) body.name = request.name;
160
- if (request.description !== void 0) body.description = request.description;
161
- if (request.icon !== void 0) body.icon = request.icon;
162
- if (request.defaults !== void 0) body.defaults = request.defaults;
163
- if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
164
- if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
165
- await this.client.workspaces.update(ref, body);
166
- return this.getWorkspace(ref, { plane: "admin" });
167
- }
168
- async deleteWorkspace(workspaceRef) {
169
- const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
170
- await this.client.workspaces.delete(ref);
171
- }
172
- /**
173
- * The API accepts only a UUID or slug as a workspace ref — a display name
174
- * gets a misleading 400 AB01 ("No update fields provided") on writes.
175
- * Match a user-supplied ref against the workspace list so name | slug |
176
- * uuid all work. Unmatched refs pass through so the API's own error stands.
177
- */
178
- async resolveWorkspaceRef(ref, planes, requireSlug = false) {
179
- for (const plane of planes) {
180
- let rows;
181
- try {
182
- rows = await this.listWorkspaces({ plane });
183
- } catch {
184
- continue;
185
- }
186
- const byRef = rows.find((w) => w.id === ref || w.slug === ref);
187
- if (byRef) return requireSlug ? byRef.slug : ref;
188
- const byName = rows.filter((w) => w.name === ref);
189
- if (byName.length > 1) {
190
- throw new Error(
191
- `workspace name '${ref}' is ambiguous (${byName.map((w) => w.slug).join(", ")}) \u2014 use a slug or UUID`
192
- );
193
- }
194
- if (byName.length === 1) return byName[0].slug;
195
- }
196
- return ref;
197
- }
198
- async getTelemetryCost(opts) {
199
- const { workspaceSlug: workspaceRef, days = 7, ...rawFilters } = opts;
200
- const filters = AIGatewayChartFiltersSchema.parse(rawFilters);
201
- if (!Number.isSafeInteger(days) || days <= 0)
202
- throw new Error("Expected days to be a positive integer");
203
- const workspaceSlug = await this.resolveWorkspaceRef(workspaceRef, ["data", "admin"], true);
204
- const raw = await this.client.telemetry.cost({
205
- workspaceSlug,
206
- days,
207
- ...filters
208
- });
209
- return {
210
- workspaceSlug,
211
- days,
212
- totalCents: raw.data.total,
213
- totalUsd: raw.data.total / 100,
214
- avgCents: raw.data.avg,
215
- avgUsd: raw.data.avg / 100,
216
- quotaExceeded: raw.data.isQuotaExceeded,
217
- records: raw.data.records.map((r) => ({
218
- date: r.x,
219
- costCents: r.y,
220
- costUsd: r.y / 100
221
- }))
222
- };
223
- }
224
- /** Re-read after a write, falling back to the (partial) write response if the get fails. */
225
- async refetchAfterWrite(workspaceRef, writeResponse) {
226
- try {
227
- return await this.getWorkspace(workspaceRef, { plane: "admin" });
228
- } catch {
229
- return normalizeWorkspaceDetail(writeResponse);
230
- }
231
- }
232
- };
233
-
234
- // src/config/client-options.ts
235
89
  function managementClientOptions(config) {
236
90
  return {
237
91
  clientId: config.mgmtClientId,
@@ -283,6 +137,207 @@ function modelSecurityClientOptions(config) {
283
137
  };
284
138
  }
285
139
 
140
+ // src/reports/agentguard.ts
141
+ async function collectAgentGuardReport(client, options = {}) {
142
+ const maxPages2 = options.maxPages ?? 40;
143
+ if (!Number.isSafeInteger(maxPages2) || maxPages2 < 1 || maxPages2 > 100)
144
+ throw new Error("maxPages must be between 1 and 100");
145
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
146
+ const end = now();
147
+ const window2 = {
148
+ start: new Date(end.getTime() - 30 * 864e5).toISOString(),
149
+ end: end.toISOString()
150
+ };
151
+ const report = {
152
+ schemaVersion: 1,
153
+ product: "Prisma AIRS AI Supply Chain \u2014 AgentGuard",
154
+ title: options.title ?? "AgentGuard environment report",
155
+ collectionStartedAt: end.toISOString(),
156
+ generatedAt: "",
157
+ window: window2,
158
+ windowLabel: "30-day scan inventory; rolling 30_DAYS statistics; current rule catalog",
159
+ schemaLabel: "Experimental browser APIs",
160
+ health: "unknown",
161
+ sources: [],
162
+ findings: [],
163
+ tables: [],
164
+ limitations: [
165
+ "Read-only evidence, not a security certification or numerical health score. Browser APIs are undocumented and may change.",
166
+ "Scans use explicit start/end timestamps; statistics use the server rolling 30_DAYS window. These independently read sources are not an atomic snapshot and need not reconcile.",
167
+ "Scan rows are not unique skills. Parent/batch rows are excluded from outcome counts to avoid counting them alongside children; per-scan vulnerability counts are never summed.",
168
+ "Rule default states describe the catalog, not effective tenant policies. No scan creation, policy changes or skill uploads are performed.",
169
+ "Missing or incomplete evidence is unknown, not zero. Scan names, identifiers, URLs, device metadata, source code and finding text are excluded."
170
+ ]
171
+ };
172
+ const label = (v, allowed) => v === null ? "UNKNOWN" : allowed.includes(v) ? v : "OTHER";
173
+ async function source(name, method, range, read) {
174
+ const s = {
175
+ name,
176
+ method,
177
+ window: range,
178
+ status: "unavailable",
179
+ pages: 0,
180
+ records: 0,
181
+ notes: []
182
+ };
183
+ report.sources.push(s);
184
+ try {
185
+ await read(s);
186
+ if (s.status !== "partial") s.status = "complete";
187
+ } catch {
188
+ s.status = s.records ? "partial" : "unavailable";
189
+ s.notes.push("Request or validation failed; no upstream error body included.");
190
+ }
191
+ if (s.status !== "complete")
192
+ report.findings.push({
193
+ priority: "review",
194
+ title: `${name} incomplete`,
195
+ evidence: s.notes.join(" "),
196
+ recommendation: "Check service access or increase the page budget, then regenerate.",
197
+ source: name
198
+ });
199
+ }
200
+ async function pages(s, read, pageCount = false) {
201
+ const rows = [];
202
+ const seen = /* @__PURE__ */ new Set();
203
+ let total;
204
+ try {
205
+ for (let page = 0; page < maxPages2; page++) {
206
+ const r = await read(rows.length);
207
+ s.pages++;
208
+ if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
209
+ throw new Error();
210
+ total ??= r.total;
211
+ if (!pageCount && (r.total !== total || rows.length + r.rows.length > total))
212
+ throw new Error();
213
+ for (const row of r.rows) {
214
+ if (!row.uuid || seen.has(row.uuid)) throw new Error();
215
+ seen.add(row.uuid);
216
+ rows.push(row);
217
+ s.records = rows.length;
218
+ }
219
+ if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
220
+ if (!r.rows.length) throw new Error();
221
+ }
222
+ s.notes.push(`Reached the ${maxPages2}-page budget.`);
223
+ throw new Error();
224
+ } catch (error) {
225
+ if (!rows.length) throw error;
226
+ s.status = "partial";
227
+ s.notes.push(
228
+ "Inventory incomplete: pagination changed, repeated, stopped early, failed or reached its budget."
229
+ );
230
+ return rows;
231
+ }
232
+ }
233
+ await source("Scans", "listScans()", `${window2.start} to ${window2.end}`, async (s) => {
234
+ const rows = await pages(s, async (skip) => {
235
+ const p = await client.listScans({
236
+ skip,
237
+ limit: 10,
238
+ start_time: window2.start,
239
+ end_time: window2.end,
240
+ isBackgroundRefresh: false
241
+ });
242
+ return { rows: p.scans, total: p.pagination.total_items };
243
+ });
244
+ const inside = rows.filter((r) => {
245
+ const t = Date.parse(r.created_at);
246
+ return Number.isFinite(t) && t >= Date.parse(window2.start) && t <= end.getTime();
247
+ });
248
+ if (inside.length !== rows.length) {
249
+ s.status = "partial";
250
+ s.notes.push("Rows with missing or out-of-window creation timestamps were excluded.");
251
+ }
252
+ const leaf = inside.filter((r) => !r.is_batch && r.child_scan_uuids.length === 0);
253
+ const groups = /* @__PURE__ */ new Map();
254
+ for (const r of leaf) {
255
+ const key = `${label(r.artifact_type, ["SKILL", "AGENT"])} / ${label(r.eval_outcome, ["BLOCKED", "ALLOWED", "PENDING", "ERROR"])}`;
256
+ groups.set(key, (groups.get(key) ?? 0) + 1);
257
+ }
258
+ report.tables.push({
259
+ title: "Scan outcomes",
260
+ note: `Collected ${s.records} rows; excluded ${inside.length - leaf.length} parent/batch rows. ${s.status === "partial" ? "Partial inventory only." : "Complete inventory for requested window."}`,
261
+ headers: ["Artifact / outcome", "Scan rows"],
262
+ rows: [...groups].sort(([a], [b]) => a.localeCompare(b))
263
+ });
264
+ const blocked = leaf.filter((r) => r.eval_outcome === "BLOCKED").length;
265
+ if (blocked)
266
+ report.findings.push({
267
+ priority: "attention",
268
+ title: "Blocked scans require review",
269
+ evidence: `${blocked} collected non-parent scan rows have BLOCKED evaluations.`,
270
+ recommendation: "Inspect the affected scans and findings before approving the artifacts.",
271
+ source: "Scans"
272
+ });
273
+ const unsettled = leaf.filter(
274
+ (r) => r.status !== "COMPLETED" || !["ALLOWED", "BLOCKED"].includes(r.eval_outcome ?? "")
275
+ ).length;
276
+ if (unsettled)
277
+ report.findings.push({
278
+ priority: "review",
279
+ title: "Scans without a completed decision",
280
+ evidence: `${unsettled} collected non-parent rows are pending, failed or have an unfamiliar status.`,
281
+ recommendation: "Review scan status; absence of a completed decision is not a passing evaluation.",
282
+ source: "Scans"
283
+ });
284
+ });
285
+ await source("Statistics", "getScanStats()", "Server rolling 30_DAYS", async (s) => {
286
+ const r = await client.getScanStats({ time_period: "30_DAYS" });
287
+ report.tables.push({
288
+ title: "Skill statistics",
289
+ note: "Server aggregates; percentage changes may be unavailable.",
290
+ headers: ["Metric", "Count", "Change (%)"],
291
+ rows: [
292
+ [
293
+ "Unique skills scanned",
294
+ r.unique_skills_scanned.count,
295
+ r.unique_skills_scanned.percent_change
296
+ ],
297
+ [
298
+ "Vulnerabilities found",
299
+ r.total_vulnerabilities_found.count,
300
+ r.total_vulnerabilities_found.percent_change
301
+ ]
302
+ ]
303
+ });
304
+ if (r.total_vulnerabilities_found.count > 0)
305
+ report.findings.push({
306
+ priority: "attention",
307
+ title: "Skill vulnerabilities reported",
308
+ evidence: `${r.total_vulnerabilities_found.count} findings reported in the server statistics window.`,
309
+ recommendation: "Review vulnerability details and remediate before use; findings are not a count of distinct vulnerable skills.",
310
+ source: "Statistics"
311
+ });
312
+ s.records = 1;
313
+ s.pages = 1;
314
+ });
315
+ await source("Rules", "listRules()", "Current catalog", async (s) => {
316
+ const rows = await pages(
317
+ s,
318
+ async (skip) => {
319
+ const p = await client.listRules({ skip, limit: 10 });
320
+ return { rows: p.rules, total: p.pagination.total_items };
321
+ },
322
+ true
323
+ );
324
+ const groups = /* @__PURE__ */ new Map();
325
+ for (const r of rows) {
326
+ const state = label(r.default_state, ["BLOCKING", "ALLOWING"]);
327
+ groups.set(state, (groups.get(state) ?? 0) + 1);
328
+ }
329
+ report.tables.push({
330
+ title: "Rule catalog defaults",
331
+ note: "These are not effective tenant policy settings.",
332
+ headers: ["Default state", "Rules"],
333
+ rows: [...groups].sort(([a], [b]) => a.localeCompare(b))
334
+ });
335
+ });
336
+ report.health = report.sources.every((s) => s.status === "unavailable") ? "unknown" : report.findings.some((f) => f.priority === "attention") ? "attention" : report.findings.length ? "review" : "no-findings";
337
+ report.generatedAt = now().toISOString();
338
+ return report;
339
+ }
340
+
286
341
  // src/cli/renderer/aigateway.ts
287
342
  import chalk4 from "chalk";
288
343
  import { dump as yamlDump } from "js-yaml";
@@ -327,7 +382,7 @@ async function resolveOutput(command, opts, resolution = {}) {
327
382
  const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
328
383
  let configured;
329
384
  try {
330
- configured = (await loadConfig()).defaultOutput;
385
+ configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
331
386
  } catch (error) {
332
387
  if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
333
388
  else throw error;
@@ -2546,13 +2601,13 @@ function statusColor4(status) {
2546
2601
  function renderRuntimeConfigHeader() {
2547
2602
  ui.header("Prisma AIRS \u2014 Runtime Configuration", "Security profile and topic management");
2548
2603
  }
2549
- function renderProfileList(profiles, format = "pretty") {
2550
- if (profiles.length === 0) {
2604
+ function renderProfileList(profiles2, format = "pretty") {
2605
+ if (profiles2.length === 0) {
2551
2606
  ui.emptyList("profiles");
2552
2607
  return;
2553
2608
  }
2554
2609
  if (format !== "pretty") {
2555
- const rows = profiles.map((p) => ({
2610
+ const rows = profiles2.map((p) => ({
2556
2611
  id: p.profileId,
2557
2612
  name: p.profileName,
2558
2613
  status: p.active ? "active" : "inactive",
@@ -2568,7 +2623,7 @@ function renderProfileList(profiles, format = "pretty") {
2568
2623
  return;
2569
2624
  }
2570
2625
  ui.section("Security Profiles:");
2571
- for (const p of profiles) {
2626
+ for (const p of profiles2) {
2572
2627
  ui.dim(p.profileId);
2573
2628
  const status = p.active ? chalk9.green("active") : chalk9.yellow("inactive");
2574
2629
  const rev = p.revision != null ? chalk9.dim(` rev:${p.revision}`) : "";
@@ -2656,13 +2711,13 @@ function renderCleanupResult(results, format = "pretty") {
2656
2711
  }
2657
2712
  console.log();
2658
2713
  }
2659
- function renderTopicList(topics, format = "pretty") {
2660
- if (topics.length === 0) {
2714
+ function renderTopicList(topics2, format = "pretty") {
2715
+ if (topics2.length === 0) {
2661
2716
  ui.emptyList("topics");
2662
2717
  return;
2663
2718
  }
2664
2719
  if (format !== "pretty") {
2665
- const rows = topics.map((t) => ({
2720
+ const rows = topics2.map((t) => ({
2666
2721
  id: t.topic_id ?? "",
2667
2722
  name: t.topic_name,
2668
2723
  revision: t.revision ?? "",
@@ -2683,7 +2738,7 @@ function renderTopicList(topics, format = "pretty") {
2683
2738
  return;
2684
2739
  }
2685
2740
  ui.section("Custom Topics:");
2686
- for (const t of topics) {
2741
+ for (const t of topics2) {
2687
2742
  ui.dim(String(t.topic_id));
2688
2743
  const rev = t.revision != null ? chalk9.dim(` rev:${t.revision}`) : "";
2689
2744
  const desc = t.description ? chalk9.dim(` \u2014 ${t.description.slice(0, 80)}`) : "";
@@ -2885,13 +2940,13 @@ function renderCustomerAppConsumption(data, format = "pretty") {
2885
2940
  }
2886
2941
  console.log();
2887
2942
  }
2888
- function renderDeploymentProfileList(profiles, format = "pretty") {
2889
- if (profiles.length === 0) {
2943
+ function renderDeploymentProfileList(profiles2, format = "pretty") {
2944
+ if (profiles2.length === 0) {
2890
2945
  ui.emptyList("deployment profiles");
2891
2946
  return;
2892
2947
  }
2893
2948
  if (format !== "pretty") {
2894
- const rows = profiles.map((p) => ({
2949
+ const rows = profiles2.map((p) => ({
2895
2950
  name: p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "",
2896
2951
  status: p.raw.status ?? "",
2897
2952
  authCode: p.raw.auth_code ?? ""
@@ -2909,18 +2964,419 @@ function renderDeploymentProfileList(profiles, format = "pretty") {
2909
2964
  );
2910
2965
  return;
2911
2966
  }
2912
- ui.section("Deployment Profiles:");
2913
- for (const p of profiles) {
2914
- const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
2915
- const status = p.raw.status;
2916
- const authCode = p.raw.auth_code;
2917
- const statusColor5 = status === "active" ? chalk9.green : chalk9.dim;
2918
- console.log(
2919
- ` ${name}${status ? ` ${statusColor5(status)}` : ""}${authCode ? ` ${chalk9.dim(authCode)}` : ""}`
2920
- );
2967
+ ui.section("Deployment Profiles:");
2968
+ for (const p of profiles2) {
2969
+ const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
2970
+ const status = p.raw.status;
2971
+ const authCode = p.raw.auth_code;
2972
+ const statusColor5 = status === "active" ? chalk9.green : chalk9.dim;
2973
+ console.log(
2974
+ ` ${name}${status ? ` ${statusColor5(status)}` : ""}${authCode ? ` ${chalk9.dim(authCode)}` : ""}`
2975
+ );
2976
+ }
2977
+ console.log();
2978
+ }
2979
+
2980
+ // src/cli/commands/agentguard.ts
2981
+ var createClient = async () => new AgentGuardClient({ ...agentGuardClientOptions(await loadConfig()), numRetries: 0 });
2982
+ var integer = (value) => {
2983
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
2984
+ usageError("Pagination requires a safe nonnegative integer");
2985
+ return Number(value);
2986
+ };
2987
+ var pageOptions = (cmd) => cmd.option("--limit <n>", "Page size", integer, 10).option("--offset <n>", "Starting offset", integer, 0);
2988
+ var readOutput = (cmd) => cmd.option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml");
2989
+ var query = (schema, value) => {
2990
+ const result = schema.safeParse(value);
2991
+ if (!result.success)
2992
+ usageError(
2993
+ "Invalid AgentGuard query. Check pagination, UUID and ISO timestamps; statistics support 30_DAYS only."
2994
+ );
2995
+ return result.data;
2996
+ };
2997
+ function emit(value, format) {
2998
+ if (format === "json") console.log(JSON.stringify(value, null, 2));
2999
+ else if (format === "yaml") console.log(dump3(value, { noRefs: true, lineWidth: -1 }).trimEnd());
3000
+ else {
3001
+ const records = Object.values(value).find(Array.isArray);
3002
+ if (records) {
3003
+ const keys = [...new Set(records.flatMap((r) => Object.keys(r)))].filter((k) => !["description", "remediation"].includes(k)).slice(0, 9);
3004
+ console.log(
3005
+ formatOutput(
3006
+ records,
3007
+ keys.map((key) => ({ key, label: key.replaceAll("_", " ") })),
3008
+ format === "pretty" ? "table" : format
3009
+ )
3010
+ );
3011
+ ui.status(`Returned ${records.length} records; see JSON/YAML for pagination metadata.`);
3012
+ return;
3013
+ }
3014
+ const rows = Object.entries(value).map(([key, v]) => ({
3015
+ key,
3016
+ value: typeof v === "object" ? JSON.stringify(v) : String(v)
3017
+ }));
3018
+ console.log(
3019
+ formatOutput(
3020
+ rows,
3021
+ [
3022
+ { key: "key", label: "Field" },
3023
+ { key: "value", label: "Value" }
3024
+ ],
3025
+ format === "pretty" ? "table" : format
3026
+ )
3027
+ );
3028
+ }
3029
+ }
3030
+ async function run(read, cmd) {
3031
+ try {
3032
+ const format = await resolveOutput(cmd, cmd.opts());
3033
+ emit(await read(), format);
3034
+ } catch (error) {
3035
+ const status = error?.statusCode;
3036
+ ui.error(
3037
+ `AgentGuard request failed${typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599 ? ` (HTTP ${status})` : ""}. Check credentials, query options and service availability. No upstream body is included.`
3038
+ );
3039
+ process.exitCode = 1;
3040
+ }
3041
+ }
3042
+ async function inventory(read, options, pageCount = false) {
3043
+ if (options.max !== void 0 && (!Number.isSafeInteger(options.max) || options.max < 0))
3044
+ usageError("--max must be a safe nonnegative integer");
3045
+ const rows = [];
3046
+ const seen = /* @__PURE__ */ new Set();
3047
+ let total;
3048
+ const cap = options.all ? options.max || Number.MAX_SAFE_INTEGER : options.limit;
3049
+ for (let page = 0; page < 1e3; page++) {
3050
+ const limit = Math.min(options.limit, cap - rows.length);
3051
+ const p = await read(options.offset + rows.length, limit);
3052
+ total ??= p.total;
3053
+ if (!pageCount && p.total !== total || p.rows.length > limit)
3054
+ throw new Error("Unstable pagination");
3055
+ for (const row of p.rows) {
3056
+ if (!row.uuid || seen.has(row.uuid)) throw new Error("Repeated identity");
3057
+ seen.add(row.uuid);
3058
+ rows.push(row);
3059
+ }
3060
+ const next = options.offset + rows.length;
3061
+ if (!pageCount && next > total && rows.length) throw new Error("Invalid total");
3062
+ const complete = pageCount ? p.rows.length < limit : next >= total;
3063
+ if (!options.all || complete || rows.length === cap)
3064
+ return {
3065
+ rows,
3066
+ pagination: {
3067
+ total_items: pageCount ? complete && options.offset === 0 ? rows.length : null : total,
3068
+ returned: rows.length,
3069
+ skip: options.offset,
3070
+ next_offset: complete ? null : next,
3071
+ truncated: !complete
3072
+ }
3073
+ };
3074
+ if (!p.rows.length) throw new Error("Premature empty page");
3075
+ }
3076
+ throw new Error("Pagination safety limit reached");
3077
+ }
3078
+ function registerAgentGuardCommand(program) {
3079
+ const group = program.command("agentguard").description("AI Supply Chain agent and skill scan telemetry (experimental, OAuth)");
3080
+ const scans = group.command("scans").description("Inspect agent and skill scans (read-only)");
3081
+ const list = readOutput(
3082
+ pageOptions(
3083
+ scans.command("list").description("List one page of scans; sensitive metadata is excluded")
3084
+ )
3085
+ ).option("--start <iso>", "Inclusive creation time start (ISO timestamp)").option("--end <iso>", "Creation time end (ISO timestamp)").action(async (opts) => {
3086
+ const options = query(AgentGuardScanListOptionsSchema, {
3087
+ skip: opts.offset,
3088
+ limit: opts.limit,
3089
+ start_time: opts.start,
3090
+ end_time: opts.end,
3091
+ isBackgroundRefresh: false
3092
+ });
3093
+ await run(async () => {
3094
+ const client = await createClient();
3095
+ const p = await inventory(async (skip, limit) => {
3096
+ const page = await client.listScans({ ...options, skip, limit });
3097
+ return { rows: page.scans, total: page.pagination.total_items };
3098
+ }, opts);
3099
+ return {
3100
+ pagination: p.pagination,
3101
+ scans: p.rows.map((r) => ({
3102
+ uuid: r.uuid,
3103
+ artifact_type: r.artifact_type,
3104
+ status: r.status,
3105
+ eval_outcome: r.eval_outcome,
3106
+ created_at: r.created_at,
3107
+ vulnerability_count: r.summary?.vulnerability_count ?? null,
3108
+ attack_chain_count: r.summary?.attack_chain_count ?? null,
3109
+ is_batch: r.is_batch,
3110
+ parent_uuid: r.parent_uuid
3111
+ }))
3112
+ };
3113
+ }, list);
3114
+ });
3115
+ const vulnerabilities = readOutput(
3116
+ scans.command("vulnerabilities <scanUuid>").description("List finding metadata; --include-content explicitly reveals sensitive details")
3117
+ ).option("--include-content", "Include source code, paths and finding text (sensitive)").action(async (id, opts) => {
3118
+ query(z.string().uuid(), id);
3119
+ await run(async () => {
3120
+ const p = await (await createClient()).listScanVulnerabilities(id);
3121
+ return {
3122
+ pagination: p.pagination,
3123
+ vulnerabilities: p.vulnerabilities.map(
3124
+ (r) => opts.includeContent ? r : {
3125
+ uuid: r.uuid,
3126
+ scan_uuid: r.scan_uuid,
3127
+ vulnerability_type: r.vulnerability_type,
3128
+ attack_chain_count: r.attack_chain_count
3129
+ }
3130
+ ),
3131
+ content_included: !!opts.includeContent
3132
+ };
3133
+ }, vulnerabilities);
3134
+ });
3135
+ const stats = readOutput(
3136
+ group.command("stats").description("Read server rolling 30-day skill statistics")
3137
+ ).option("--time-period <period>", "Verified period: 30_DAYS", "30_DAYS").action(async (opts) => {
3138
+ const options = query(AgentGuardStatsOptionsSchema, { time_period: opts.timePeriod });
3139
+ await run(async () => (await createClient()).getScanStats(options), stats);
3140
+ });
3141
+ const rulesGroup = group.command("rules").description("Rule catalog (not effective tenant policy)");
3142
+ const rules = readOutput(
3143
+ pageOptions(rulesGroup.command("list").description("List rule catalog defaults"))
3144
+ ).action(async (opts) => {
3145
+ const options = query(AgentGuardPageOptionsSchema, { skip: opts.offset, limit: opts.limit });
3146
+ await run(async () => {
3147
+ const client = await createClient();
3148
+ const p = await inventory(
3149
+ async (skip, limit) => {
3150
+ const page = await client.listRules({ ...options, skip, limit });
3151
+ return { rows: page.rules, total: page.pagination.total_items };
3152
+ },
3153
+ opts,
3154
+ true
3155
+ );
3156
+ return { pagination: p.pagination, rules: p.rows };
3157
+ }, rules);
3158
+ });
3159
+ group.command("report").description("Generate an aggregate-only 30-day HTML/Markdown report").option("--output <format>", "Deliverable: html (default) or markdown").option("--output-file <path>", "New file in CWD by default; - for stdout; never overwrite").option("--max-pages <n>", "Page budget per inventory (1\u2013100, 10 rows/page)", integer, 40).option("--title <text>", "Report title", "AgentGuard environment report").option("--strict", "Exit 1 after writing if any source is incomplete").action(async (opts) => {
3160
+ const format = opts.output ?? program.opts().output ?? "html";
3161
+ if (!["html", "markdown"].includes(format))
3162
+ usageError("Report output must be html or markdown");
3163
+ if (opts.maxPages < 1 || opts.maxPages > 100)
3164
+ usageError("--max-pages must be between 1 and 100");
3165
+ if (!opts.title.trim() || opts.title.length > 240)
3166
+ usageError("--title must contain 1\u2013240 characters");
3167
+ if (opts.outputFile !== void 0 && !opts.outputFile.trim())
3168
+ usageError("--output-file cannot be empty");
3169
+ const path3 = opts.outputFile === "-" ? void 0 : resolve(
3170
+ opts.outputFile ?? `airs-agentguard-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID().slice(0, 8)}.${format === "html" ? "html" : "md"}`
3171
+ );
3172
+ try {
3173
+ if (path3) {
3174
+ const exists = await lstat(path3).catch((e) => {
3175
+ if (e.code !== "ENOENT") throw e;
3176
+ return void 0;
3177
+ });
3178
+ if (exists) throw new Error();
3179
+ }
3180
+ const data = await collectAgentGuardReport(await createClient(), {
3181
+ title: opts.title,
3182
+ maxPages: opts.maxPages
3183
+ });
3184
+ const output = format === "html" ? renderEnvironmentReportHtml(data) : renderEnvironmentReportMarkdown(data);
3185
+ if (path3) {
3186
+ await writeReportFile(path3, output);
3187
+ ui.status(`Report \u2192 ${path3.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
3188
+ } else
3189
+ await new Promise(
3190
+ (done, reject) => process.stdout.write(output, (error) => error ? reject(error) : done())
3191
+ );
3192
+ const incomplete = data.sources.filter((s) => s.status !== "complete").length;
3193
+ if (incomplete)
3194
+ ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
3195
+ if (data.health === "unknown" || opts.strict && incomplete) process.exitCode = 1;
3196
+ } catch {
3197
+ ui.error(
3198
+ "AgentGuard report failed. Check credentials and a writable, new output path. Existing files are never overwritten."
3199
+ );
3200
+ process.exitCode = 1;
3201
+ }
3202
+ });
3203
+ }
3204
+
3205
+ // src/cli/commands/aigateway.ts
3206
+ import {
3207
+ GatewayDefaultsInputSchema,
3208
+ GatewayRateLimitInputSchema,
3209
+ GatewayUsageLimitInputSchema
3210
+ } from "@cdot65/prisma-airs-sdk";
3211
+
3212
+ // src/airs/aigateway.ts
3213
+ import {
3214
+ AIGatewayChartFiltersSchema,
3215
+ AIGatewayClient
3216
+ } from "@cdot65/prisma-airs-sdk";
3217
+ function toLimitArray(value) {
3218
+ if (Array.isArray(value)) return value;
3219
+ if (value !== null && typeof value === "object") return [value];
3220
+ return [];
3221
+ }
3222
+ function normalizeWorkspace(raw) {
3223
+ return {
3224
+ id: raw.id,
3225
+ slug: raw.slug,
3226
+ name: raw.name,
3227
+ icon: raw.icon,
3228
+ description: raw.description,
3229
+ createdAt: raw.created_at,
3230
+ lastUpdatedAt: raw.last_updated_at,
3231
+ isDefault: Boolean(raw.is_default),
3232
+ status: raw.status,
3233
+ scopeName: raw.scope_name
3234
+ };
3235
+ }
3236
+ function normalizeWorkspaceDetail(raw) {
3237
+ return {
3238
+ ...normalizeWorkspace(raw),
3239
+ defaults: raw.defaults,
3240
+ usageLimits: toLimitArray(raw.usage_limits),
3241
+ rateLimits: toLimitArray(raw.rate_limits),
3242
+ securitySettings: raw.security_settings,
3243
+ dataPlaneSecuritySettings: raw.data_plane_security_settings,
3244
+ settings: raw.settings
3245
+ };
3246
+ }
3247
+ function aiGatewayGrantHint(err) {
3248
+ const status = err?.status ?? err?.statusCode;
3249
+ if (status !== 403) return void 0;
3250
+ const message = err instanceof Error ? err.message : String(err);
3251
+ const grant = message.includes("AB03") ? "the service account is missing a workspace-scope grant (data plane, /ai_gw/v2)" : "the service account is missing a tenant-root admin grant (admin plane, /ai_gw/admin/v2)";
3252
+ return `${grant}. SCM Access Management edits the existing role row by default \u2014 use "Add Role" so the account ends up with both role rows, not one row moved.`;
3253
+ }
3254
+ var SdkAiGatewayService = class {
3255
+ client;
3256
+ constructor(opts) {
3257
+ this.client = new AIGatewayClient(opts);
3258
+ }
3259
+ async listWorkspaces(options) {
3260
+ const response = await this.client.workspaces.list(options);
3261
+ return response.data.map(normalizeWorkspace);
3262
+ }
3263
+ async listAllWorkspaces() {
3264
+ const [active, archived] = await Promise.all([
3265
+ this.client.workspaces.list({ plane: "admin" }),
3266
+ this.client.workspaces.list({ plane: "admin", status: "archived" })
3267
+ ]);
3268
+ return [
3269
+ ...active.data,
3270
+ ...archived.data
3271
+ ].map(normalizeWorkspace);
3272
+ }
3273
+ async getWorkspace(workspaceRef, options) {
3274
+ try {
3275
+ const raw = await this.client.workspaces.get(workspaceRef, options);
3276
+ return normalizeWorkspaceDetail(raw);
3277
+ } catch (err) {
3278
+ const status = err.statusCode;
3279
+ if (status !== 404) throw err;
3280
+ const resolved = await this.resolveWorkspaceRef(workspaceRef, [
3281
+ options?.plane ?? "data",
3282
+ "admin"
3283
+ ]);
3284
+ if (resolved === workspaceRef) throw err;
3285
+ const raw = await this.client.workspaces.get(resolved, options);
3286
+ return normalizeWorkspaceDetail(raw);
3287
+ }
3288
+ }
3289
+ async createWorkspace(request) {
3290
+ const body = {
3291
+ name: request.name,
3292
+ scope_name: request.scopeName
3293
+ };
3294
+ if (request.description !== void 0) body.description = request.description;
3295
+ if (request.icon !== void 0) body.icon = request.icon;
3296
+ if (request.defaults !== void 0) body.defaults = request.defaults;
3297
+ if (request.users !== void 0) body.users = request.users;
3298
+ if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
3299
+ if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
3300
+ const created = await this.client.workspaces.create(body);
3301
+ return this.refetchAfterWrite(created.id, created);
3302
+ }
3303
+ async updateWorkspace(workspaceRef, request) {
3304
+ const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
3305
+ const body = {};
3306
+ if (request.name !== void 0) body.name = request.name;
3307
+ if (request.description !== void 0) body.description = request.description;
3308
+ if (request.icon !== void 0) body.icon = request.icon;
3309
+ if (request.defaults !== void 0) body.defaults = request.defaults;
3310
+ if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
3311
+ if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
3312
+ await this.client.workspaces.update(ref, body);
3313
+ return this.getWorkspace(ref, { plane: "admin" });
3314
+ }
3315
+ async deleteWorkspace(workspaceRef) {
3316
+ const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
3317
+ await this.client.workspaces.delete(ref);
3318
+ }
3319
+ /**
3320
+ * The API accepts only a UUID or slug as a workspace ref — a display name
3321
+ * gets a misleading 400 AB01 ("No update fields provided") on writes.
3322
+ * Match a user-supplied ref against the workspace list so name | slug |
3323
+ * uuid all work. Unmatched refs pass through so the API's own error stands.
3324
+ */
3325
+ async resolveWorkspaceRef(ref, planes, requireSlug = false) {
3326
+ for (const plane of planes) {
3327
+ let rows;
3328
+ try {
3329
+ rows = await this.listWorkspaces({ plane });
3330
+ } catch {
3331
+ continue;
3332
+ }
3333
+ const byRef = rows.find((w) => w.id === ref || w.slug === ref);
3334
+ if (byRef) return requireSlug ? byRef.slug : ref;
3335
+ const byName = rows.filter((w) => w.name === ref);
3336
+ if (byName.length > 1) {
3337
+ throw new Error(
3338
+ `workspace name '${ref}' is ambiguous (${byName.map((w) => w.slug).join(", ")}) \u2014 use a slug or UUID`
3339
+ );
3340
+ }
3341
+ if (byName.length === 1) return byName[0].slug;
3342
+ }
3343
+ return ref;
3344
+ }
3345
+ async getTelemetryCost(opts) {
3346
+ const { workspaceSlug: workspaceRef, days = 7, ...rawFilters } = opts;
3347
+ const filters = AIGatewayChartFiltersSchema.parse(rawFilters);
3348
+ if (!Number.isSafeInteger(days) || days <= 0)
3349
+ throw new Error("Expected days to be a positive integer");
3350
+ const workspaceSlug = await this.resolveWorkspaceRef(workspaceRef, ["data", "admin"], true);
3351
+ const raw = await this.client.telemetry.cost({
3352
+ workspaceSlug,
3353
+ days,
3354
+ ...filters
3355
+ });
3356
+ return {
3357
+ workspaceSlug,
3358
+ days,
3359
+ totalCents: raw.data.total,
3360
+ totalUsd: raw.data.total / 100,
3361
+ avgCents: raw.data.avg,
3362
+ avgUsd: raw.data.avg / 100,
3363
+ quotaExceeded: raw.data.isQuotaExceeded,
3364
+ records: raw.data.records.map((r) => ({
3365
+ date: r.x,
3366
+ costCents: r.y,
3367
+ costUsd: r.y / 100
3368
+ }))
3369
+ };
3370
+ }
3371
+ /** Re-read after a write, falling back to the (partial) write response if the get fails. */
3372
+ async refetchAfterWrite(workspaceRef, writeResponse) {
3373
+ try {
3374
+ return await this.getWorkspace(workspaceRef, { plane: "admin" });
3375
+ } catch {
3376
+ return normalizeWorkspaceDetail(writeResponse);
3377
+ }
2921
3378
  }
2922
- console.log();
2923
- }
3379
+ };
2924
3380
 
2925
3381
  // src/cli/confirm.ts
2926
3382
  async function confirmOrAbort(message, force, options = {}) {
@@ -2958,8 +3414,8 @@ import {
2958
3414
  GatewayInferenceInputCreateEmbeddingRequestSchema,
2959
3415
  GatewayInferenceInputCreateResponseSchema
2960
3416
  } from "@cdot65/prisma-airs-sdk";
2961
- import { dump as dump3 } from "js-yaml";
2962
- function integer(raw, flag) {
3417
+ import { dump as dump4 } from "js-yaml";
3418
+ function integer2(raw, flag) {
2963
3419
  const value = Number(raw);
2964
3420
  if (!Number.isSafeInteger(value) || value < 1)
2965
3421
  throw new CliUsageError(`${flag} must be a positive integer`);
@@ -2992,11 +3448,11 @@ async function inferenceBody(kind, prompt, opts, defaultModel) {
2992
3448
  body.model = model;
2993
3449
  if (opts.stream !== void 0) body.stream = opts.stream;
2994
3450
  if (opts.maxTokens !== void 0)
2995
- body[kind === "responses" ? "max_output_tokens" : "max_completion_tokens"] = integer(
3451
+ body[kind === "responses" ? "max_output_tokens" : "max_completion_tokens"] = integer2(
2996
3452
  opts.maxTokens,
2997
3453
  "--max-tokens"
2998
3454
  );
2999
- if (opts.dimensions !== void 0) body.dimensions = integer(opts.dimensions, "--dimensions");
3455
+ if (opts.dimensions !== void 0) body.dimensions = integer2(opts.dimensions, "--dimensions");
3000
3456
  if (opts.encoding !== void 0) body.encoding_format = opts.encoding;
3001
3457
  if (kind === "responses" && body.store === void 0) body.store = false;
3002
3458
  return body;
@@ -3031,7 +3487,7 @@ function registerAiGatewayInference(parent) {
3031
3487
  const outputError = (error) => controller.abort(error);
3032
3488
  let stream;
3033
3489
  let failed = false;
3034
- let failure;
3490
+ let failure2;
3035
3491
  try {
3036
3492
  const format = await resolveOutput(command, opts, { allowed: ["pretty", "json", "yaml"] });
3037
3493
  const config = await loadConfig({ aiGwInferenceEndpoint: opts.endpoint });
@@ -3052,14 +3508,14 @@ function registerAiGatewayInference(parent) {
3052
3508
  const client = new AIGatewayInferenceClient({
3053
3509
  endpoint: config.aiGwInferenceEndpoint,
3054
3510
  apiKey: config.aiGwInferenceApiKey,
3055
- timeoutMs: integer(opts.timeout ?? "60000", "--timeout"),
3511
+ timeoutMs: integer2(opts.timeout ?? "60000", "--timeout"),
3056
3512
  numRetries: 0
3057
3513
  });
3058
3514
  process.once("SIGINT", interrupt);
3059
3515
  process.once("SIGTERM", interrupt);
3060
3516
  process.stdout.on("error", outputError);
3061
- const write = (text2) => new Promise((resolve6, reject) => {
3062
- process.stdout.write(text2, (error) => error ? reject(error) : resolve6());
3517
+ const write = (text2) => new Promise((resolve8, reject) => {
3518
+ process.stdout.write(text2, (error) => error ? reject(error) : resolve8());
3063
3519
  });
3064
3520
  const requestOptions = { signal: controller.signal };
3065
3521
  const result = kind === "chat" ? await client.createChatCompletion(
@@ -3086,7 +3542,7 @@ function registerAiGatewayInference(parent) {
3086
3542
  }
3087
3543
  if (format === "pretty") await write("\n");
3088
3544
  } else {
3089
- const text2 = format === "yaml" ? dump3(result, { noRefs: true }).trimEnd() : format === "pretty" && kind !== "embeddings" ? responseText(result) : JSON.stringify(result, null, 2);
3545
+ const text2 = format === "yaml" ? dump4(result, { noRefs: true }).trimEnd() : format === "pretty" && kind !== "embeddings" ? responseText(result) : JSON.stringify(result, null, 2);
3090
3546
  await write(`${text2}
3091
3547
  `);
3092
3548
  if ("status" in result && (result.status === "failed" || result.status === "incomplete"))
@@ -3094,7 +3550,7 @@ function registerAiGatewayInference(parent) {
3094
3550
  }
3095
3551
  } catch (error) {
3096
3552
  failed = true;
3097
- failure = error;
3553
+ failure2 = error;
3098
3554
  } finally {
3099
3555
  await stream?.cancel();
3100
3556
  controller.abort();
@@ -3102,10 +3558,10 @@ function registerAiGatewayInference(parent) {
3102
3558
  process.removeListener("SIGTERM", interrupt);
3103
3559
  process.stdout.removeListener("error", outputError);
3104
3560
  }
3105
- if (failed && failure?.code !== "EPIPE") {
3106
- if (failure?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
3107
- usageError(failure instanceof Error ? failure.message : "Invalid inference options");
3108
- 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);
3109
3565
  }
3110
3566
  });
3111
3567
  }
@@ -3224,7 +3680,7 @@ function headersToRecord(headers) {
3224
3680
  }
3225
3681
  return headers;
3226
3682
  }
3227
- function installDebugLogger(logPath) {
3683
+ function installDebugLogger(logPath, options = {}) {
3228
3684
  mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
3229
3685
  writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
3230
3686
  const originalFetch = globalThis.fetch;
@@ -3239,7 +3695,7 @@ function installDebugLogger(logPath) {
3239
3695
  if (!isAirsUrl(url) && !inference) {
3240
3696
  return originalFetch(input, init2);
3241
3697
  }
3242
- const dashboard = /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url);
3698
+ const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
3243
3699
  const method = init2?.method ?? (input instanceof Request ? input.method : "GET");
3244
3700
  const reqHeaders = redactHeaders(rawHeaders);
3245
3701
  const loggedUrl = redactUrl(url);
@@ -3317,7 +3773,7 @@ function installDebugLogger(logPath) {
3317
3773
  // src/cli/commands/aigateway/shared.ts
3318
3774
  import { open, readFile as readFile2, unlink } from "fs/promises";
3319
3775
  import { AIGatewayClient as AIGatewayClient2 } from "@cdot65/prisma-airs-sdk";
3320
- import { dump as dump4, load } from "js-yaml";
3776
+ import { dump as dump5, load } from "js-yaml";
3321
3777
  async function defaultClientFactory() {
3322
3778
  const config = await loadConfig();
3323
3779
  return new AIGatewayClient2(aiGatewayClientOptions(config));
@@ -3398,7 +3854,7 @@ function renderGatewayList(items, format, label) {
3398
3854
  return;
3399
3855
  }
3400
3856
  if (format === "yaml") {
3401
- console.log(dump4(items, { noRefs: true, lineWidth: -1 }).trimEnd());
3857
+ console.log(dump5(items, { noRefs: true, lineWidth: -1 }).trimEnd());
3402
3858
  return;
3403
3859
  }
3404
3860
  if (items.length === 0) {
@@ -3416,7 +3872,7 @@ function renderGatewayDetail(value, format) {
3416
3872
  return;
3417
3873
  }
3418
3874
  if (format === "yaml") {
3419
- console.log(dump4(item, { noRefs: true, lineWidth: -1 }).trimEnd());
3875
+ console.log(dump5(item, { noRefs: true, lineWidth: -1 }).trimEnd());
3420
3876
  return;
3421
3877
  }
3422
3878
  const rows = Object.entries(item).map(([key, raw]) => ({
@@ -3433,20 +3889,20 @@ function renderGatewayDetail(value, format) {
3433
3889
  );
3434
3890
  if (rendered) console.log(rendered);
3435
3891
  }
3436
- async function runList(command, opts, label, load2) {
3892
+ async function runList(command, opts, label, load3) {
3437
3893
  try {
3438
3894
  const format = await resolveOutput(command, opts);
3439
3895
  const client = await createAiGatewayClient();
3440
- renderGatewayList(responseItems(await load2(client)), format, label);
3896
+ renderGatewayList(responseItems(await load3(client)), format, label);
3441
3897
  } catch (error) {
3442
3898
  failAiGateway(error);
3443
3899
  }
3444
3900
  }
3445
- async function runDetail(command, opts, load2) {
3901
+ async function runDetail(command, opts, load3) {
3446
3902
  try {
3447
3903
  const format = await resolveOutput(command, opts);
3448
3904
  const client = await createAiGatewayClient();
3449
- renderGatewayDetail(await load2(client), format);
3905
+ renderGatewayDetail(await load3(client), format);
3450
3906
  } catch (error) {
3451
3907
  failAiGateway(error);
3452
3908
  }
@@ -4747,11 +5203,11 @@ function parseNonnegativePage(value) {
4747
5203
  }
4748
5204
 
4749
5205
  // src/cli/commands/aigateway-report.ts
4750
- import { lstat } from "fs/promises";
4751
- import { resolve } from "path";
5206
+ import { lstat as lstat2 } from "fs/promises";
5207
+ import { resolve as resolve2 } from "path";
4752
5208
  import { AIGatewayClient as AIGatewayClient3 } from "@cdot65/prisma-airs-sdk";
4753
- import { z } from "zod";
4754
- import { randomUUID } from "crypto";
5209
+ import { z as z2 } from "zod";
5210
+ import { randomUUID as randomUUID2 } from "crypto";
4755
5211
  function registerAiGatewayReportCommand(aigateway) {
4756
5212
  const command = aigateway.command("report").alias("dashboard").description("Generate an AI Gateway environment report (read-only)").requiredOption("--workspace <ref>", "Workspace slug, UUID, or unique display name").option("--days <n>", "Rolling window in days (default: 1)", "1").option("--start <iso>", "Explicit ISO-8601 start; requires --end").option("--end <iso>", "Explicit ISO-8601 end; requires --start").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
4757
5213
  "--output-file <path>",
@@ -4778,21 +5234,21 @@ function registerAiGatewayReportCommand(aigateway) {
4778
5234
  usageError("Use --days or --start/--end, not both");
4779
5235
  const end = opts.end ? new Date(opts.end) : /* @__PURE__ */ new Date();
4780
5236
  const start = opts.start ? new Date(opts.start) : new Date(end.getTime() - Number(opts.days) * 864e5);
4781
- const iso = z.string().datetime({ offset: true });
5237
+ const iso = z2.string().datetime({ offset: true });
4782
5238
  if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
4783
5239
  usageError("Provide a valid increasing ISO-8601 window with timezone");
4784
- const maxPages = Number(opts.maxPages);
4785
- 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)
4786
5242
  usageError("--max-pages must be an integer from 1 to 100");
4787
5243
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
4788
5244
  usageError("--title must contain 1\u2013240 characters");
4789
5245
  if (opts.outputFile !== void 0 && !opts.outputFile.trim())
4790
5246
  usageError("--output-file cannot be empty");
4791
- const defaultName = `airs-aigateway-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
4792
- const outputPath = opts.outputFile === "-" ? void 0 : resolve(opts.outputFile ?? defaultName);
5247
+ const defaultName = `airs-aigateway-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID2().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
5248
+ const outputPath = opts.outputFile === "-" ? void 0 : resolve2(opts.outputFile ?? defaultName);
4793
5249
  try {
4794
5250
  if (outputPath) {
4795
- const existing = await lstat(outputPath).catch((error) => {
5251
+ const existing = await lstat2(outputPath).catch((error) => {
4796
5252
  if (error.code !== "ENOENT") throw error;
4797
5253
  return void 0;
4798
5254
  });
@@ -4806,7 +5262,7 @@ function registerAiGatewayReportCommand(aigateway) {
4806
5262
  ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
4807
5263
  const report = await collectGatewayEnvironmentReport(client, {
4808
5264
  title: opts.title,
4809
- maxPages,
5265
+ maxPages: maxPages2,
4810
5266
  workspace: opts.workspace,
4811
5267
  tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
4812
5268
  start,
@@ -5339,7 +5795,7 @@ function registerConfigCommand(program) {
5339
5795
  }
5340
5796
 
5341
5797
  // src/cli/commands/doctor.ts
5342
- import { randomUUID as randomUUID2 } from "crypto";
5798
+ import { randomUUID as randomUUID3 } from "crypto";
5343
5799
  import { readFile as readFile4 } from "fs/promises";
5344
5800
  import { init, Scanner } from "@cdot65/prisma-airs-sdk";
5345
5801
  var DOCTOR_TIMEOUT_MS = 5e3;
@@ -5437,8 +5893,8 @@ async function withTimeout(promise, ms) {
5437
5893
  try {
5438
5894
  return await Promise.race([
5439
5895
  promise,
5440
- new Promise((resolve6) => {
5441
- timer = setTimeout(() => resolve6(TIMED_OUT), ms);
5896
+ new Promise((resolve8) => {
5897
+ timer = setTimeout(() => resolve8(TIMED_OUT), ms);
5442
5898
  })
5443
5899
  ]);
5444
5900
  } finally {
@@ -5593,7 +6049,7 @@ async function defaultScannerProbe() {
5593
6049
  const config = await loadConfig();
5594
6050
  init(runtimeInitOptions(config));
5595
6051
  const scanner = new Scanner();
5596
- return scanner.queryByScanIds([randomUUID2()]);
6052
+ return scanner.queryByScanIds([randomUUID3()]);
5597
6053
  }
5598
6054
  async function defaultMgmtProbe() {
5599
6055
  const config = await loadConfig();
@@ -5603,8 +6059,8 @@ async function defaultMgmtProbe() {
5603
6059
  tsgId: config.mgmtTsgId,
5604
6060
  tokenEndpoint: config.mgmtTokenEndpoint
5605
6061
  });
5606
- const topics = await service.listTopics();
5607
- return topics.length;
6062
+ const topics2 = await service.listTopics();
6063
+ return topics2.length;
5608
6064
  }
5609
6065
  async function defaultAiGwProbe() {
5610
6066
  const config = await loadConfig();
@@ -5698,6 +6154,21 @@ function registerDoctorCommand(program) {
5698
6154
  // src/cli/commands/modelsecurity.ts
5699
6155
  import { execFileSync, spawn } from "child_process";
5700
6156
  import * as fs from "fs";
6157
+ import { z as z3 } from "zod";
6158
+ function supplyChainWindow(opts) {
6159
+ const parsed = z3.object({
6160
+ start: z3.string().datetime({ offset: true }).optional(),
6161
+ end: z3.string().datetime({ offset: true }).optional(),
6162
+ modelVersion: z3.string().uuid().optional()
6163
+ }).refine((v) => !v.start || !v.end || Date.parse(v.start) <= Date.parse(v.end)).safeParse(opts);
6164
+ if (!parsed.success)
6165
+ usageError("Use valid ISO timestamps with start <= end and a valid model-version UUID.");
6166
+ return {
6167
+ startTime: parsed.data.start,
6168
+ endTime: parsed.data.end,
6169
+ modelVersionUuid: parsed.data.modelVersion
6170
+ };
6171
+ }
5701
6172
  var VALID_EXTRAS = ["all", "aws", "gcp", "azure", "artifactory", "gitlab"];
5702
6173
  function hasBin(bin) {
5703
6174
  try {
@@ -5707,13 +6178,13 @@ function hasBin(bin) {
5707
6178
  return false;
5708
6179
  }
5709
6180
  }
5710
- function run(bin, args, label) {
5711
- return new Promise((resolve6, reject) => {
6181
+ function run2(bin, args, label) {
6182
+ return new Promise((resolve8, reject) => {
5712
6183
  const child = spawn(bin, args, { stdio: "inherit" });
5713
6184
  child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
5714
6185
  child.on("close", (code) => {
5715
6186
  if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
5716
- else resolve6();
6187
+ else resolve8();
5717
6188
  });
5718
6189
  });
5719
6190
  }
@@ -5847,7 +6318,7 @@ function registerModelSecurityCommand(program) {
5847
6318
  }
5848
6319
  for (const step of steps) {
5849
6320
  ui.status(`\u2192 ${step.label}`);
5850
- await run(step.bin, step.args, step.label);
6321
+ await run2(step.bin, step.args, step.label);
5851
6322
  }
5852
6323
  ui.success("model-security-client installed successfully.");
5853
6324
  if (useUv) {
@@ -6003,7 +6474,7 @@ function registerModelSecurityCommand(program) {
6003
6474
  }
6004
6475
  });
6005
6476
  const scans = ms.command("scans").description("Model security scan operations");
6006
- const scansList = scans.command("list").description("List model security scans").option("--eval-outcome <outcome>", "Filter by eval outcome").option("--source-type <type>", "Filter by source type").option("--scan-origin <origin>", "Filter by scan origin").option("--search <query>", "Search scans").option("--limit <n>", "Max results", "20").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6477
+ const scansList = scans.command("list").description("List model security scans").option("--model-version <uuid>", "Filter scans by model version UUID").option("--start <iso>", "Creation time start (ISO timestamp)").option("--end <iso>", "Creation time end (ISO timestamp)").option("--eval-outcome <outcome>", "Filter by eval outcome").option("--source-type <type>", "Filter by source type").option("--scan-origin <origin>", "Filter by scan origin").option("--search <query>", "Search scans").option("--limit <n>", "Max results", "20").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6007
6478
  "after",
6008
6479
  examples(
6009
6480
  "airs model-security scans list",
@@ -6017,6 +6488,7 @@ function registerModelSecurityCommand(program) {
6017
6488
  const service = await createService2();
6018
6489
  const listOptions = {
6019
6490
  evalOutcome: opts.evalOutcome,
6491
+ ...supplyChainWindow(opts),
6020
6492
  sourceType: opts.sourceType,
6021
6493
  scanOrigin: opts.scanOrigin,
6022
6494
  search: opts.search,
@@ -6111,7 +6583,7 @@ function registerModelSecurityCommand(program) {
6111
6583
  }
6112
6584
  });
6113
6585
  const models = ms.command("models").description("Browse the scanned model catalog (read-only)");
6114
- const modelsList = models.command("list").description("List models in the catalog").option("--search <text>", "Filter by search text").option("--search-query <text>", "Filter by model UUID or name").option("--sort-field <field>", "Sort field: created_at, updated_at").option("--sort-order <order>", "Sort order: asc, desc").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText("after", examples("airs model-security models list")).action(async (opts) => {
6586
+ const modelsList = models.command("list").description("List models in the catalog").option("--start <iso>", "Creation time start (ISO timestamp)").option("--end <iso>", "Creation time end (ISO timestamp)").option("--search <text>", "Filter by search text").option("--search-query <text>", "Filter by model UUID or name").option("--sort-field <field>", "Sort field: created_at, updated_at").option("--sort-order <order>", "Sort order: asc, desc").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText("after", examples("airs model-security models list")).action(async (opts) => {
6115
6587
  try {
6116
6588
  const fmt = await resolveOutput(modelsList, opts);
6117
6589
  if (fmt === "pretty") renderModelSecurityHeader();
@@ -6119,6 +6591,7 @@ function registerModelSecurityCommand(program) {
6119
6591
  const listOptions = {
6120
6592
  search: opts.search,
6121
6593
  searchQuery: opts.searchQuery,
6594
+ ...supplyChainWindow(opts),
6122
6595
  sortField: opts.sortField,
6123
6596
  sortOrder: opts.sortOrder,
6124
6597
  limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
@@ -6292,10 +6765,10 @@ async function backupTargets(opts) {
6292
6765
  }
6293
6766
 
6294
6767
  // src/cli/commands/redteam-report.ts
6295
- import { lstat as lstat2 } from "fs/promises";
6296
- import { resolve as resolve2 } from "path";
6768
+ import { lstat as lstat3 } from "fs/promises";
6769
+ import { resolve as resolve3 } from "path";
6297
6770
  import { RedTeamClient } from "@cdot65/prisma-airs-sdk";
6298
- import { randomUUID as randomUUID3 } from "crypto";
6771
+ import { randomUUID as randomUUID4 } from "crypto";
6299
6772
  function registerRedTeamReportCommand(redteam, scanReport) {
6300
6773
  const command = redteam.command("report [jobId]").alias("dashboard").description("Generate an environment report, or view an individual scan report by job ID").option("--attacks", "Individual scan only: include attack list", false).option("--severity <level>", "Individual scan only: filter attacks by severity").option("--limit <n>", "Individual scan only: max attacks to show", "20").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
6301
6774
  "--output-file <path>",
@@ -6321,18 +6794,18 @@ function registerRedTeamReportCommand(redteam, scanReport) {
6321
6794
  const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
6322
6795
  if (format !== "html" && format !== "markdown")
6323
6796
  usageError("Report output must be html or markdown");
6324
- const maxPages = Number(opts.maxPages);
6325
- 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)
6326
6799
  usageError("--max-pages must be an integer from 1 to 100");
6327
6800
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
6328
6801
  usageError("--title must contain 1\u2013240 characters");
6329
6802
  if (opts.outputFile !== void 0 && !opts.outputFile.trim())
6330
6803
  usageError("--output-file cannot be empty");
6331
- const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID3().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
6332
- const outputPath = opts.outputFile === "-" ? void 0 : resolve2(opts.outputFile ?? defaultName);
6804
+ const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID4().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
6805
+ const outputPath = opts.outputFile === "-" ? void 0 : resolve3(opts.outputFile ?? defaultName);
6333
6806
  try {
6334
6807
  if (outputPath) {
6335
- const existing = await lstat2(outputPath).catch((error) => {
6808
+ const existing = await lstat3(outputPath).catch((error) => {
6336
6809
  if (error.code !== "ENOENT") throw error;
6337
6810
  return void 0;
6338
6811
  });
@@ -6346,7 +6819,7 @@ function registerRedTeamReportCommand(redteam, scanReport) {
6346
6819
  ui.status("Collecting Red Team statistics and current configuration (read-only)...");
6347
6820
  const report = await collectRedTeamEnvironmentReport(client, {
6348
6821
  title: opts.title,
6349
- maxPages
6822
+ maxPages: maxPages2
6350
6823
  });
6351
6824
  const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
6352
6825
  if (outputPath) {
@@ -7605,9 +8078,9 @@ function registerRedteamCommand(program) {
7605
8078
  }
7606
8079
 
7607
8080
  // src/cli/commands/runtime.ts
7608
- import { randomUUID as randomUUID7 } from "crypto";
8081
+ import { randomUUID as randomUUID9 } from "crypto";
7609
8082
  import * as fs5 from "fs";
7610
- import { readFile as readFile10 } from "fs/promises";
8083
+ import { readFile as readFile11 } from "fs/promises";
7611
8084
  import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
7612
8085
  import chalk11 from "chalk";
7613
8086
 
@@ -7671,9 +8144,9 @@ function buildDataProtection(flags) {
7671
8144
  let hasAny = false;
7672
8145
  if (flags.dlpAction) {
7673
8146
  const dld = { action: flags.dlpAction };
7674
- const profiles = parseList(flags.dlpProfiles);
7675
- if (profiles) {
7676
- dld.member = profiles.map((text2) => ({ text: text2 }));
8147
+ const profiles2 = parseList(flags.dlpProfiles);
8148
+ if (profiles2) {
8149
+ dld.member = profiles2.map((text2) => ({ text: text2 }));
7677
8150
  }
7678
8151
  if (flags.maskDataInline != null) {
7679
8152
  dld["mask-data-inline"] = flags.maskDataInline;
@@ -7853,7 +8326,7 @@ function mergeProfilePolicy(existing, overrides) {
7853
8326
  }
7854
8327
 
7855
8328
  // src/cli/bulk-scan-lock.ts
7856
- import { randomUUID as randomUUID4 } from "crypto";
8329
+ import { randomUUID as randomUUID5 } from "crypto";
7857
8330
  import * as fs3 from "fs/promises";
7858
8331
  function errorCode(error) {
7859
8332
  return error?.code;
@@ -7884,7 +8357,7 @@ function processIsAlive(pid) {
7884
8357
  }
7885
8358
  }
7886
8359
  async function installLock(lockPath, record) {
7887
- const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID4()}`;
8360
+ const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID5()}`;
7888
8361
  try {
7889
8362
  await fs3.writeFile(candidate, JSON.stringify(record), {
7890
8363
  encoding: "utf-8",
@@ -7902,7 +8375,7 @@ async function acquireBulkScanLock(statePath) {
7902
8375
  version: 1,
7903
8376
  pid: process.pid,
7904
8377
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7905
- token: randomUUID4()
8378
+ token: randomUUID5()
7906
8379
  };
7907
8380
  for (let attempt = 0; attempt < 2; attempt++) {
7908
8381
  try {
@@ -7938,32 +8411,32 @@ async function acquireBulkScanLock(statePath) {
7938
8411
  }
7939
8412
 
7940
8413
  // src/cli/bulk-scan-state.ts
7941
- import { randomUUID as randomUUID5 } from "crypto";
8414
+ import { randomUUID as randomUUID6 } from "crypto";
7942
8415
  import * as fs4 from "fs/promises";
7943
8416
  import * as path2 from "path";
7944
- import { z as z2 } from "zod";
7945
- var BulkScanResultSchema = z2.object({
7946
- index: z2.number().int().nonnegative(),
7947
- reqId: z2.number().int().nonnegative(),
7948
- prompt: z2.string(),
7949
- response: z2.string().optional(),
7950
- scanId: z2.string(),
7951
- reportId: z2.string(),
7952
- action: z2.enum(["allow", "block", "failed"]),
7953
- category: z2.string(),
7954
- triggered: z2.boolean(),
7955
- detections: z2.record(z2.boolean()),
7956
- error: z2.string().optional()
8417
+ import { z as z4 } from "zod";
8418
+ var BulkScanResultSchema = z4.object({
8419
+ index: z4.number().int().nonnegative(),
8420
+ reqId: z4.number().int().nonnegative(),
8421
+ prompt: z4.string(),
8422
+ response: z4.string().optional(),
8423
+ scanId: z4.string(),
8424
+ reportId: z4.string(),
8425
+ action: z4.enum(["allow", "block", "failed"]),
8426
+ category: z4.string(),
8427
+ triggered: z4.boolean(),
8428
+ detections: z4.record(z4.boolean()),
8429
+ error: z4.string().optional()
7957
8430
  });
7958
- var BulkScanItemSchema = z2.object({
7959
- index: z2.number().int().nonnegative(),
7960
- reqId: z2.number().int().nonnegative(),
7961
- prompt: z2.string(),
7962
- status: z2.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
7963
- scanId: z2.string().min(1).optional(),
7964
- receiptReportId: z2.string().optional(),
8431
+ var BulkScanItemSchema = z4.object({
8432
+ index: z4.number().int().nonnegative(),
8433
+ reqId: z4.number().int().nonnegative(),
8434
+ prompt: z4.string(),
8435
+ status: z4.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
8436
+ scanId: z4.string().min(1).optional(),
8437
+ receiptReportId: z4.string().optional(),
7965
8438
  result: BulkScanResultSchema.optional(),
7966
- error: z2.string().optional()
8439
+ error: z4.string().optional()
7967
8440
  }).superRefine((item, ctx) => {
7968
8441
  if (item.reqId !== item.index) {
7969
8442
  ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
@@ -8001,15 +8474,15 @@ var BulkScanItemSchema = z2.object({
8001
8474
  }
8002
8475
  }
8003
8476
  });
8004
- var BulkScanStateSchema = z2.object({
8005
- version: z2.literal(2),
8006
- profile: z2.string().min(1),
8007
- sessionId: z2.string().optional(),
8008
- outputFile: z2.string().min(1),
8009
- batchSize: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8010
- createdAt: z2.string().datetime(),
8011
- updatedAt: z2.string().datetime(),
8012
- items: z2.array(BulkScanItemSchema).min(1)
8477
+ var BulkScanStateSchema = z4.object({
8478
+ version: z4.literal(2),
8479
+ profile: z4.string().min(1),
8480
+ sessionId: z4.string().optional(),
8481
+ outputFile: z4.string().min(1),
8482
+ batchSize: z4.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8483
+ createdAt: z4.string().datetime(),
8484
+ updatedAt: z4.string().datetime(),
8485
+ items: z4.array(BulkScanItemSchema).min(1)
8013
8486
  }).superRefine((state, ctx) => {
8014
8487
  const indices = /* @__PURE__ */ new Set();
8015
8488
  for (const [position, item] of state.items.entries()) {
@@ -8035,8 +8508,8 @@ async function saveBulkScanState(state, dir, filePath) {
8035
8508
  }
8036
8509
  await fs4.mkdir(dir, { recursive: true, mode: 448 });
8037
8510
  if (!filePath) await fs4.chmod(dir, 448);
8038
- const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID5()}.bulk-scan.json`);
8039
- const temporary = `${target}.tmp-${process.pid}-${randomUUID5()}`;
8511
+ const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID6()}.bulk-scan.json`);
8512
+ const temporary = `${target}.tmp-${process.pid}-${randomUUID6()}`;
8040
8513
  try {
8041
8514
  await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
8042
8515
  encoding: "utf-8",
@@ -9059,11 +9532,655 @@ function registerDlpCommands(runtime) {
9059
9532
  register3(dlp);
9060
9533
  }
9061
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
+
9062
10179
  // src/cli/commands/profiles-cleanup.ts
9063
10180
  import { execSync } from "child_process";
9064
- function findDuplicateProfiles(profiles) {
10181
+ function findDuplicateProfiles(profiles2) {
9065
10182
  const groups = /* @__PURE__ */ new Map();
9066
- for (const p of profiles) {
10183
+ for (const p of profiles2) {
9067
10184
  const rev = p.revision ?? 0;
9068
10185
  const entry = { id: p.profileId, revision: rev };
9069
10186
  const existing = groups.get(p.profileName);
@@ -9161,11 +10278,11 @@ function registerCleanupCommand(parent) {
9161
10278
  }
9162
10279
 
9163
10280
  // src/cli/commands/runtime-dashboard.ts
9164
- import { lstat as lstat3 } from "fs/promises";
9165
- import { resolve as resolve4 } from "path";
9166
- import { dump as dump5 } from "js-yaml";
10281
+ import { lstat as lstat4 } from "fs/promises";
10282
+ import { resolve as resolve6 } from "path";
10283
+ import { dump as dump7 } from "js-yaml";
9167
10284
  var TIME_UNITS = ["hour", "hours", "day", "days"];
9168
- function integer2(value, name, minimum = 0) {
10285
+ function integer3(value, name, minimum = 0) {
9169
10286
  const n = Number(value);
9170
10287
  if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(n) || n < minimum)
9171
10288
  throw new CliUsageError(`${name} must be a safe integer >= ${minimum}`);
@@ -9177,7 +10294,7 @@ function text(value, name) {
9177
10294
  }
9178
10295
  function window(opts) {
9179
10296
  return {
9180
- timeInterval: integer2(opts.interval, "--interval", 1),
10297
+ timeInterval: integer3(opts.interval, "--interval", 1),
9181
10298
  timeUnit: text(opts.unit, "--unit")
9182
10299
  };
9183
10300
  }
@@ -9192,7 +10309,7 @@ function identity(opts) {
9192
10309
  function scan(opts) {
9193
10310
  return {
9194
10311
  scanId: text(opts.scanId, "--scan-id"),
9195
- scanSubReqId: integer2(opts.scanSubReqId, "--scan-sub-req-id")
10312
+ scanSubReqId: integer3(opts.scanSubReqId, "--scan-sub-req-id")
9196
10313
  };
9197
10314
  }
9198
10315
  function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_UNITS) {
@@ -9209,7 +10326,7 @@ function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_
9209
10326
  unit
9210
10327
  );
9211
10328
  }
9212
- function pageOptions(command) {
10329
+ function pageOptions2(command) {
9213
10330
  return command.option("--limit <n>", "Items per page", "25").option("--offset <n>", "Item offset", "0");
9214
10331
  }
9215
10332
  function sessionOptions(command) {
@@ -9238,8 +10355,8 @@ function registerRuntimeDashboardCommands(runtime) {
9238
10355
  "Scan content requires exactly one of --show-content or --output-file <new-path>"
9239
10356
  );
9240
10357
  if (opts.outputFile) {
9241
- const path3 = resolve4(text(opts.outputFile, "--output-file"));
9242
- const existing = await lstat3(path3).catch((error) => {
10358
+ const path3 = resolve6(text(opts.outputFile, "--output-file"));
10359
+ const existing = await lstat4(path3).catch((error) => {
9243
10360
  if (error.code !== "ENOENT") throw error;
9244
10361
  return void 0;
9245
10362
  });
@@ -9252,9 +10369,9 @@ function registerRuntimeDashboardCommands(runtime) {
9252
10369
  numRetries: 0
9253
10370
  });
9254
10371
  const result = await read(client.dashboard);
9255
- const output = format === "yaml" ? dump5(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);
9256
10373
  if (command.name() === "scan-content" && opts.outputFile) {
9257
- await writeReportFile(resolve4(String(opts.outputFile)), `${output}
10374
+ await writeReportFile(resolve6(String(opts.outputFile)), `${output}
9258
10375
  `);
9259
10376
  ui.status("Scan content exported to the requested private file (0600).");
9260
10377
  } else console.log(output);
@@ -9269,7 +10386,7 @@ function registerRuntimeDashboardCommands(runtime) {
9269
10386
  });
9270
10387
  }
9271
10388
  register6(
9272
- pageOptions(
10389
+ pageOptions2(
9273
10390
  timeOptions(
9274
10391
  dashboard.command("applications").description("One page of application activity"),
9275
10392
  "1",
@@ -9278,19 +10395,19 @@ function registerRuntimeDashboardCommands(runtime) {
9278
10395
  )
9279
10396
  ),
9280
10397
  (opts) => {
9281
- const interval = integer2(opts.interval, "--interval", 1);
10398
+ const interval = integer3(opts.interval, "--interval", 1);
9282
10399
  const unit = text(opts.unit, "--unit");
9283
10400
  if (![1, 7, 30, 60].includes(interval) || !["day", "days", "hour"].includes(unit))
9284
10401
  throw new CliUsageError(
9285
10402
  "Application overview supports interval 1/7/30/60 and unit day/days/hour"
9286
10403
  );
9287
- const query = {
10404
+ const query2 = {
9288
10405
  timeInterval: interval,
9289
10406
  timeUnit: unit,
9290
- limit: integer2(opts.limit, "--limit", 1),
9291
- offset: integer2(opts.offset, "--offset")
10407
+ limit: integer3(opts.limit, "--limit", 1),
10408
+ offset: integer3(opts.offset, "--offset")
9292
10409
  };
9293
- return (client) => client.applicationsOverview(query);
10410
+ return (client) => client.applicationsOverview(query2);
9294
10411
  }
9295
10412
  );
9296
10413
  for (const [name, method] of [
@@ -9305,16 +10422,16 @@ function registerRuntimeDashboardCommands(runtime) {
9305
10422
  ["days"]
9306
10423
  ),
9307
10424
  (opts) => {
9308
- const interval = integer2(opts.interval, "--interval", 1);
10425
+ const interval = integer3(opts.interval, "--interval", 1);
9309
10426
  if (![7, 30, 60].includes(interval) || opts.unit !== "days")
9310
10427
  throw new CliUsageError("Application detail requires 7, 30, or 60 days");
9311
- const query = {
10428
+ const query2 = {
9312
10429
  appId: text(opts.appId, "--app-id"),
9313
10430
  appName: text(opts.appName, "--app-name"),
9314
10431
  timeInterval: interval,
9315
10432
  timeUnit: "days"
9316
10433
  };
9317
- return (client) => client[method](query);
10434
+ return (client) => client[method](query2);
9318
10435
  }
9319
10436
  );
9320
10437
  }
@@ -9326,35 +10443,35 @@ function registerRuntimeDashboardCommands(runtime) {
9326
10443
  register6(
9327
10444
  timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
9328
10445
  (opts) => {
9329
- const query = window(opts);
9330
- return (client) => client[method](query);
10446
+ const query2 = window(opts);
10447
+ return (client) => client[method](query2);
9331
10448
  }
9332
10449
  );
9333
10450
  }
9334
10451
  register6(timeOptions(sessions.command("chart")), (opts) => {
9335
- const query = window(opts);
9336
- return (client) => client.sessionsChart(query);
10452
+ const query2 = window(opts);
10453
+ return (client) => client.sessionsChart(query2);
9337
10454
  });
9338
10455
  register6(
9339
- pageOptions(
10456
+ pageOptions2(
9340
10457
  timeOptions(
9341
10458
  sessions.command("list").description("Session entries; --all walks pages with completeness checks")
9342
10459
  )
9343
10460
  ).option("--all", "Walk all pages (up to 100 pages)").option("--max <n>", "Item cap with --all; 0 removes item cap", "10000"),
9344
10461
  (opts) => {
9345
- const query = {
10462
+ const query2 = {
9346
10463
  ...window(opts),
9347
- limit: integer2(opts.limit, "--limit", 1),
9348
- offset: integer2(opts.offset, "--offset")
10464
+ limit: integer3(opts.limit, "--limit", 1),
10465
+ offset: integer3(opts.offset, "--offset")
9349
10466
  };
9350
- const maximum = integer2(opts.max, "--max");
10467
+ const maximum = integer3(opts.max, "--max");
9351
10468
  return async (client) => {
9352
10469
  const items = [];
9353
10470
  const seen = /* @__PURE__ */ new Set();
9354
10471
  let expectedTotal;
9355
- let offset = query.offset;
10472
+ let offset = query2.offset;
9356
10473
  for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
9357
- const page = await client.sessionsOverview({ ...query, offset });
10474
+ const page = await client.sessionsOverview({ ...query2, offset });
9358
10475
  if (!opts.all) {
9359
10476
  ui.status(
9360
10477
  `Returned ${page.items.length} of ${page.pagination.total_items ?? "unknown"} sessions; this is one page.`
@@ -9362,7 +10479,7 @@ function registerRuntimeDashboardCommands(runtime) {
9362
10479
  return page.items;
9363
10480
  }
9364
10481
  const total = page.pagination.total_items;
9365
- if (!Number.isSafeInteger(total) || total < 0 || page.pagination.skip !== offset || page.pagination.limit !== query.limit || page.items.length > query.limit || expectedTotal !== void 0 && total !== expectedTotal)
10482
+ if (!Number.isSafeInteger(total) || total < 0 || page.pagination.skip !== offset || page.pagination.limit !== query2.limit || page.items.length > query2.limit || expectedTotal !== void 0 && total !== expectedTotal)
9366
10483
  throw new Error("Unstable pagination");
9367
10484
  expectedTotal = total;
9368
10485
  for (const item of page.items) {
@@ -9385,7 +10502,7 @@ function registerRuntimeDashboardCommands(runtime) {
9385
10502
  }
9386
10503
  if (offset === total) {
9387
10504
  ui.status(
9388
- `Retrieved ${items.length} sessions from offset ${query.offset}; pagination complete.`
10505
+ `Retrieved ${items.length} sessions from offset ${query2.offset}; pagination complete.`
9389
10506
  );
9390
10507
  return items;
9391
10508
  }
@@ -9401,33 +10518,33 @@ function registerRuntimeDashboardCommands(runtime) {
9401
10518
  };
9402
10519
  }
9403
10520
  );
9404
- register6(pageOptions(sessionOptions(sessions.command("get"))), (opts) => {
9405
- const query = {
10521
+ register6(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
10522
+ const query2 = {
9406
10523
  ...identity(opts),
9407
- limit: integer2(opts.limit, "--limit", 1),
9408
- offset: integer2(opts.offset, "--offset")
10524
+ limit: integer3(opts.limit, "--limit", 1),
10525
+ offset: integer3(opts.offset, "--offset")
9409
10526
  };
9410
- return (client) => client.session(query);
10527
+ return (client) => client.session(query2);
9411
10528
  });
9412
10529
  register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
9413
- const query = { ...identity(opts), ...scan(opts) };
9414
- return (client) => client.sessionTransaction(query);
10530
+ const query2 = { ...identity(opts), ...scan(opts) };
10531
+ return (client) => client.sessionTransaction(query2);
9415
10532
  });
9416
10533
  register6(
9417
10534
  scanOptions(
9418
10535
  sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
9419
10536
  ).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
9420
10537
  (opts) => {
9421
- const query = scan(opts);
9422
- return (client) => client.scanContent(query);
10538
+ const query2 = scan(opts);
10539
+ return (client) => client.scanContent(query2);
9423
10540
  }
9424
10541
  );
9425
10542
  }
9426
10543
 
9427
10544
  // src/cli/commands/runtime-report.ts
9428
- import { lstat as lstat4 } from "fs/promises";
9429
- import { resolve as resolve5 } from "path";
9430
- import { randomUUID as randomUUID6 } from "crypto";
10545
+ import { lstat as lstat5 } from "fs/promises";
10546
+ import { resolve as resolve7 } from "path";
10547
+ import { randomUUID as randomUUID8 } from "crypto";
9431
10548
  function registerRuntimeReportCommand(runtime) {
9432
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(
9433
10550
  "--output-file <path>",
@@ -9444,18 +10561,18 @@ function registerRuntimeReportCommand(runtime) {
9444
10561
  const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
9445
10562
  if (format !== "html" && format !== "markdown")
9446
10563
  usageError("Report output must be html or markdown");
9447
- const maxPages = Number(opts.maxPages);
9448
- 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)
9449
10566
  usageError("--max-pages must be an integer from 1 to 100");
9450
10567
  if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
9451
10568
  usageError("--title must contain 1\u2013240 characters");
9452
10569
  if (opts.outputFile !== void 0 && !opts.outputFile.trim())
9453
10570
  usageError("--output-file cannot be empty");
9454
- const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID6().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
9455
- const outputPath = opts.outputFile === "-" ? void 0 : resolve5(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);
9456
10573
  try {
9457
10574
  if (outputPath) {
9458
- const existing = await lstat4(outputPath).catch((error) => {
10575
+ const existing = await lstat5(outputPath).catch((error) => {
9459
10576
  if (error.code !== "ENOENT") throw error;
9460
10577
  return void 0;
9461
10578
  });
@@ -9467,7 +10584,7 @@ function registerRuntimeReportCommand(runtime) {
9467
10584
  numRetries: 0
9468
10585
  });
9469
10586
  ui.status("Collecting daily runtime activity and current configuration (read-only)...");
9470
- const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages });
10587
+ const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages: maxPages2 });
9471
10588
  const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
9472
10589
  if (outputPath) {
9473
10590
  await writeReportFile(outputPath, rendered);
@@ -9627,7 +10744,7 @@ function registerCreateCommand(parent) {
9627
10744
  }
9628
10745
 
9629
10746
  // src/cli/commands/topics-eval.ts
9630
- import { readFile as readFile9 } from "fs/promises";
10747
+ import { readFile as readFile10 } from "fs/promises";
9631
10748
 
9632
10749
  // src/core/prompt-loader.ts
9633
10750
  function parseCsvLine(line) {
@@ -9770,7 +10887,7 @@ function registerEvalCommand(parent) {
9770
10887
  resolveDeprecatedAliases(cmd, opts);
9771
10888
  try {
9772
10889
  const config = await loadConfig();
9773
- const csvContent = await readFile9(opts.prompts, "utf-8");
10890
+ const csvContent = await readFile10(opts.prompts, "utf-8");
9774
10891
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
9775
10892
  if (!config.airsApiKey && !config.airsApiToken) {
9776
10893
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
@@ -9794,8 +10911,8 @@ function registerEvalCommand(parent) {
9794
10911
 
9795
10912
  // src/cli/commands/topics-revert.ts
9796
10913
  async function revertTopic(mgmt, profileName, topicName) {
9797
- const topics = await mgmt.listTopics();
9798
- 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);
9799
10916
  if (!match || !match.topic_id) {
9800
10917
  throw new Error(`Topic "${topicName}" not found`);
9801
10918
  }
@@ -9942,7 +11059,7 @@ function completedBulkResults(state) {
9942
11059
  }
9943
11060
  async function writeBulkResults(outputPath, results) {
9944
11061
  await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
9945
- const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID7()}`;
11062
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID9()}`;
9946
11063
  try {
9947
11064
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
9948
11065
  encoding: "utf-8",
@@ -10069,7 +11186,7 @@ function registerRuntimeCommand(program) {
10069
11186
  if (!config.airsApiKey && !config.airsApiToken) {
10070
11187
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
10071
11188
  }
10072
- const raw = await readFile10(opts.file, "utf-8");
11189
+ const raw = await readFile11(opts.file, "utf-8");
10073
11190
  const prompts = parseInputFile(raw, opts.file);
10074
11191
  if (prompts.length === 0) {
10075
11192
  usageError("No prompts found in input file");
@@ -10284,16 +11401,17 @@ function registerRuntimeCommand(program) {
10284
11401
  const fmt = await resolveOutput(deploymentProfilesList, opts);
10285
11402
  if (fmt === "pretty") renderRuntimeConfigHeader();
10286
11403
  const service = await createMgmtService();
10287
- const profiles2 = await service.listDeploymentProfiles({
11404
+ const profiles3 = await service.listDeploymentProfiles({
10288
11405
  unactivated: opts.unactivated
10289
11406
  });
10290
- renderDeploymentProfileList(profiles2, fmt);
11407
+ renderDeploymentProfileList(profiles3, fmt);
10291
11408
  } catch (err) {
10292
11409
  fail(err);
10293
11410
  }
10294
11411
  });
10295
- const profiles = runtime.command("profiles").description("Manage AIRS security profiles");
10296
- 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(
10297
11415
  "after",
10298
11416
  examples(
10299
11417
  "airs runtime profiles list",
@@ -10332,7 +11450,7 @@ function registerRuntimeCommand(program) {
10332
11450
  fail(err);
10333
11451
  }
10334
11452
  });
10335
- 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) => {
10336
11454
  try {
10337
11455
  const fmt = await resolveOutput(profilesGet, opts);
10338
11456
  if (fmt === "pretty") renderRuntimeConfigHeader();
@@ -10341,10 +11459,10 @@ function registerRuntimeCommand(program) {
10341
11459
  nameOrId
10342
11460
  );
10343
11461
  if (opts.revision !== void 0 || opts.allVersions) {
10344
- const profiles2 = (await service.listAllProfiles({ latest: false })).filter(
11462
+ const profiles3 = (await service.listAllProfiles({ latest: false })).filter(
10345
11463
  (profile) => isUuid ? profile.profileId === nameOrId : profile.profileName === nameOrId
10346
11464
  );
10347
- 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);
10348
11466
  if (selected.length === 0) throw new Error(`Profile ${nameOrId} not found`);
10349
11467
  if (opts.allVersions) emitList(profilesView, selected, fmt);
10350
11468
  else emitDetail(profilesView, selected[0], fmt);
@@ -10356,7 +11474,7 @@ function registerRuntimeCommand(program) {
10356
11474
  fail(err);
10357
11475
  }
10358
11476
  });
10359
- 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) => {
10360
11478
  const service = await createMgmtService();
10361
11479
  try {
10362
11480
  renderRuntimeConfigHeader();
@@ -10411,7 +11529,7 @@ function registerRuntimeCommand(program) {
10411
11529
  fail(err);
10412
11530
  }
10413
11531
  });
10414
- 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) => {
10415
11533
  try {
10416
11534
  renderRuntimeConfigHeader();
10417
11535
  const service = await createMgmtService();
@@ -10460,7 +11578,7 @@ function registerRuntimeCommand(program) {
10460
11578
  fail(err);
10461
11579
  }
10462
11580
  });
10463
- 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) => {
10464
11582
  try {
10465
11583
  renderRuntimeConfigHeader();
10466
11584
  const service = await createMgmtService();
@@ -10493,7 +11611,7 @@ function registerRuntimeCommand(program) {
10493
11611
  fail(err);
10494
11612
  }
10495
11613
  });
10496
- registerCleanupCommand(profiles);
11614
+ registerCleanupCommand(profiles2);
10497
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");
10498
11616
  registerDeprecatedAlias(resumePoll, {
10499
11617
  oldFlag: "--output <file>",
@@ -10641,10 +11759,10 @@ function registerRuntimeCommand(program) {
10641
11759
  );
10642
11760
  process.exitCode = 1;
10643
11761
  });
10644
- const topics = runtime.command("topics").description("Manage AIRS custom topics and guardrail generation");
10645
- registerApplyCommand(topics);
10646
- registerCreateCommand(topics);
10647
- 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) => {
10648
11766
  try {
10649
11767
  renderRuntimeConfigHeader();
10650
11768
  const service = await createMgmtService();
@@ -10662,8 +11780,8 @@ function registerRuntimeCommand(program) {
10662
11780
  fail(err);
10663
11781
  }
10664
11782
  });
10665
- registerEvalCommand(topics);
10666
- 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) => {
10667
11785
  try {
10668
11786
  const fmt = await resolveOutput(topicsGet, opts);
10669
11787
  if (fmt === "pretty") renderRuntimeConfigHeader();
@@ -10672,10 +11790,10 @@ function registerRuntimeCommand(program) {
10672
11790
  nameOrId
10673
11791
  );
10674
11792
  if (opts.revision !== void 0 || opts.allVersions) {
10675
- const topics2 = (await service.listTopics()).filter(
11793
+ const topics3 = (await service.listTopics()).filter(
10676
11794
  (topic) => isUuid ? topic.topic_id === nameOrId : topic.topic_name === nameOrId
10677
11795
  );
10678
- 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);
10679
11797
  if (selected.length === 0) throw new Error(`Topic ${nameOrId} not found`);
10680
11798
  if (opts.allVersions) emitList(topicsView, selected, fmt);
10681
11799
  else emitDetail(topicsView, selected[0], fmt);
@@ -10687,7 +11805,7 @@ function registerRuntimeCommand(program) {
10687
11805
  fail(err);
10688
11806
  }
10689
11807
  });
10690
- 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) => {
10691
11809
  try {
10692
11810
  const fmt = await resolveOutput(topicsList, opts);
10693
11811
  const params = resolveListParams(topicsList, opts, { dialect: "offset" });
@@ -10708,9 +11826,9 @@ function registerRuntimeCommand(program) {
10708
11826
  fail(err);
10709
11827
  }
10710
11828
  });
10711
- registerRevertCommand(topics);
10712
- registerSampleCommand(topics);
10713
- 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) => {
10714
11832
  try {
10715
11833
  renderRuntimeConfigHeader();
10716
11834
  const service = await createMgmtService();
@@ -10725,6 +11843,114 @@ function registerRuntimeCommand(program) {
10725
11843
  registerDlpCommands(runtime);
10726
11844
  }
10727
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
+
10728
11954
  // src/cli/program.ts
10729
11955
  var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
10730
11956
  "categories",
@@ -10745,7 +11971,8 @@ var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
10745
11971
  "version",
10746
11972
  "versions",
10747
11973
  "violation",
10748
- "violations"
11974
+ "violations",
11975
+ "vulnerabilities"
10749
11976
  ]);
10750
11977
  function applyListDeleteAliases(cmd) {
10751
11978
  for (const sub of cmd.commands) {
@@ -10787,12 +12014,19 @@ function buildProgram() {
10787
12014
  program.hook("preAction", async (_thisCommand, actionCommand) => {
10788
12015
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
10789
12016
  setQuiet(Boolean(root.quiet));
10790
- const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway"].includes(actionCommand.parent?.name() ?? "") && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
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
+ );
12022
+ const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway", "agentguard"].includes(
12023
+ actionCommand.parent?.name() ?? ""
12024
+ ) && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
10791
12025
  if (isEnvironmentReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
10792
12026
  usageError(
10793
12027
  "Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
10794
12028
  );
10795
- 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")) {
10796
12030
  try {
10797
12031
  const format = await resolveOutput(actionCommand, actionCommand.opts());
10798
12032
  actionCommand.setOptionValueWithSource("output", format, "implied");
@@ -10803,10 +12037,16 @@ function buildProgram() {
10803
12037
  if (root.debug) {
10804
12038
  const logPath = join4(
10805
12039
  process.cwd(),
10806
- `debug-api-${Date.now()}-${randomUUID8().slice(0, 8)}.jsonl`
12040
+ `debug-api-${Date.now()}-${randomUUID10().slice(0, 8)}.jsonl`
10807
12041
  );
10808
12042
  try {
10809
- installDebugLogger(logPath);
12043
+ let ancestor = actionCommand;
12044
+ let agentGuard = false;
12045
+ while (ancestor) {
12046
+ if (ancestor.name() === "agentguard") agentGuard = true;
12047
+ ancestor = ancestor.parent;
12048
+ }
12049
+ installDebugLogger(logPath, { omitBodies: agentGuard });
10810
12050
  } catch {
10811
12051
  ui.error(
10812
12052
  "Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
@@ -10819,8 +12059,10 @@ function buildProgram() {
10819
12059
  registerRuntimeCommand(program);
10820
12060
  registerRedteamCommand(program);
10821
12061
  registerModelSecurityCommand(program);
12062
+ registerAgentGuardCommand(program);
10822
12063
  registerAiGatewayCommand(program);
10823
12064
  registerConfigCommand(program);
12065
+ registerTenantCommand(program);
10824
12066
  registerDoctorCommand(program);
10825
12067
  registerCompletionCommand(program);
10826
12068
  applyListDeleteAliases(program);