@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/README.md +3 -1
- package/dist/{chunk-7Z76VCB3.js → chunk-NKAVUBKI.js} +232 -27
- package/dist/cli/index.js +1621 -379
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -13,11 +13,17 @@ import {
|
|
|
13
13
|
collectRedTeamEnvironmentReport,
|
|
14
14
|
collectRuntimeDailyReport,
|
|
15
15
|
computeMetrics,
|
|
16
|
+
createTenant,
|
|
17
|
+
defaultTenantConfigPath,
|
|
18
|
+
deleteTenant,
|
|
19
|
+
expandConfigPath,
|
|
16
20
|
getOrCreateManagementClient,
|
|
17
21
|
inspectConfig,
|
|
18
22
|
loadConfig,
|
|
19
23
|
readBackupDir,
|
|
20
24
|
readBackupFile,
|
|
25
|
+
readTenantConfig,
|
|
26
|
+
readTenantStore,
|
|
21
27
|
renderEnvironmentReportHtml,
|
|
22
28
|
renderEnvironmentReportMarkdown,
|
|
23
29
|
renderRedTeamReportHtml,
|
|
@@ -27,10 +33,11 @@ import {
|
|
|
27
33
|
resolveConfigFilePath,
|
|
28
34
|
resolveOutputDir,
|
|
29
35
|
sanitizeFilename,
|
|
36
|
+
switchTenant,
|
|
30
37
|
validateTopic,
|
|
31
38
|
writeBackupFile,
|
|
32
39
|
writeReportFile
|
|
33
|
-
} from "../chunk-
|
|
40
|
+
} from "../chunk-NKAVUBKI.js";
|
|
34
41
|
|
|
35
42
|
// src/cli/index.ts
|
|
36
43
|
import "dotenv/config";
|
|
@@ -49,189 +56,36 @@ function installProcessGuards() {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
// src/cli/program.ts
|
|
52
|
-
import { randomUUID as
|
|
59
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
53
60
|
import { readFileSync as readFileSync4 } from "fs";
|
|
54
61
|
import { dirname as dirname4, join as join4 } from "path";
|
|
55
62
|
import { fileURLToPath } from "url";
|
|
56
63
|
import { Command } from "commander";
|
|
57
64
|
|
|
58
|
-
// src/cli/commands/
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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/
|
|
66
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
2550
|
-
if (
|
|
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 =
|
|
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
|
|
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(
|
|
2660
|
-
if (
|
|
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 =
|
|
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
|
|
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(
|
|
2889
|
-
if (
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
2962
|
-
function
|
|
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"] =
|
|
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 =
|
|
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
|
|
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:
|
|
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((
|
|
3062
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3517
|
+
const write = (text2) => new Promise((resolve8, reject) => {
|
|
3518
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve8());
|
|
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" ?
|
|
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
|
-
|
|
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 &&
|
|
3106
|
-
if (
|
|
3107
|
-
usageError(
|
|
3108
|
-
fail(
|
|
3561
|
+
if (failed && failure2?.code !== "EPIPE") {
|
|
3562
|
+
if (failure2?.errorType === ErrorType.USER_REQUEST_PAYLOAD_ERROR)
|
|
3563
|
+
usageError(failure2 instanceof Error ? failure2.message : "Invalid inference options");
|
|
3564
|
+
fail(failure2);
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
4785
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
5240
|
+
const maxPages2 = Number(opts.maxPages);
|
|
5241
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
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(":", "-")}-${
|
|
4792
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
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
|
|
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
|
|
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((
|
|
5441
|
-
timer = setTimeout(() =>
|
|
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([
|
|
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
|
|
5607
|
-
return
|
|
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
|
|
5711
|
-
return new Promise((
|
|
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
|
|
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
|
|
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
|
|
6296
|
-
import { resolve as
|
|
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
|
|
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
|
|
6325
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
6797
|
+
const maxPages2 = Number(opts.maxPages);
|
|
6798
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
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(":", "-")}-${
|
|
6332
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
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
|
|
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
|
|
8081
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
7609
8082
|
import * as fs5 from "fs";
|
|
7610
|
-
import { readFile as
|
|
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
|
|
7675
|
-
if (
|
|
7676
|
-
dld.member =
|
|
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
|
|
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}-${
|
|
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:
|
|
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
|
|
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
|
|
7945
|
-
var BulkScanResultSchema =
|
|
7946
|
-
index:
|
|
7947
|
-
reqId:
|
|
7948
|
-
prompt:
|
|
7949
|
-
response:
|
|
7950
|
-
scanId:
|
|
7951
|
-
reportId:
|
|
7952
|
-
action:
|
|
7953
|
-
category:
|
|
7954
|
-
triggered:
|
|
7955
|
-
detections:
|
|
7956
|
-
error:
|
|
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 =
|
|
7959
|
-
index:
|
|
7960
|
-
reqId:
|
|
7961
|
-
prompt:
|
|
7962
|
-
status:
|
|
7963
|
-
scanId:
|
|
7964
|
-
receiptReportId:
|
|
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:
|
|
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 =
|
|
8005
|
-
version:
|
|
8006
|
-
profile:
|
|
8007
|
-
sessionId:
|
|
8008
|
-
outputFile:
|
|
8009
|
-
batchSize:
|
|
8010
|
-
createdAt:
|
|
8011
|
-
updatedAt:
|
|
8012
|
-
items:
|
|
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, "-")}-${
|
|
8039
|
-
const temporary = `${target}.tmp-${process.pid}-${
|
|
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(
|
|
10181
|
+
function findDuplicateProfiles(profiles2) {
|
|
9065
10182
|
const groups = /* @__PURE__ */ new Map();
|
|
9066
|
-
for (const p of
|
|
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
|
|
9165
|
-
import { resolve as
|
|
9166
|
-
import { dump as
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
9242
|
-
const existing = await
|
|
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" ?
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
10404
|
+
const query2 = {
|
|
9288
10405
|
timeInterval: interval,
|
|
9289
10406
|
timeUnit: unit,
|
|
9290
|
-
limit:
|
|
9291
|
-
offset:
|
|
10407
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
10408
|
+
offset: integer3(opts.offset, "--offset")
|
|
9292
10409
|
};
|
|
9293
|
-
return (client) => client.applicationsOverview(
|
|
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 =
|
|
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
|
|
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](
|
|
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
|
|
9330
|
-
return (client) => client[method](
|
|
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
|
|
9336
|
-
return (client) => client.sessionsChart(
|
|
10452
|
+
const query2 = window(opts);
|
|
10453
|
+
return (client) => client.sessionsChart(query2);
|
|
9337
10454
|
});
|
|
9338
10455
|
register6(
|
|
9339
|
-
|
|
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
|
|
10462
|
+
const query2 = {
|
|
9346
10463
|
...window(opts),
|
|
9347
|
-
limit:
|
|
9348
|
-
offset:
|
|
10464
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
10465
|
+
offset: integer3(opts.offset, "--offset")
|
|
9349
10466
|
};
|
|
9350
|
-
const maximum =
|
|
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 =
|
|
10472
|
+
let offset = query2.offset;
|
|
9356
10473
|
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
9357
|
-
const page = await client.sessionsOverview({ ...
|
|
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 !==
|
|
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 ${
|
|
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(
|
|
9405
|
-
const
|
|
10521
|
+
register6(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
|
|
10522
|
+
const query2 = {
|
|
9406
10523
|
...identity(opts),
|
|
9407
|
-
limit:
|
|
9408
|
-
offset:
|
|
10524
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
10525
|
+
offset: integer3(opts.offset, "--offset")
|
|
9409
10526
|
};
|
|
9410
|
-
return (client) => client.session(
|
|
10527
|
+
return (client) => client.session(query2);
|
|
9411
10528
|
});
|
|
9412
10529
|
register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
|
|
9413
|
-
const
|
|
9414
|
-
return (client) => client.sessionTransaction(
|
|
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
|
|
9422
|
-
return (client) => client.scanContent(
|
|
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
|
|
9429
|
-
import { resolve as
|
|
9430
|
-
import { randomUUID as
|
|
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
|
|
9448
|
-
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(
|
|
10564
|
+
const maxPages2 = Number(opts.maxPages);
|
|
10565
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages2) || maxPages2 > 100)
|
|
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(":", "-")}-${
|
|
9455
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
10571
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID8().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
10572
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve7(opts.outputFile ?? defaultName);
|
|
9456
10573
|
try {
|
|
9457
10574
|
if (outputPath) {
|
|
9458
|
-
const existing = await
|
|
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
|
|
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
|
|
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
|
|
9798
|
-
const match =
|
|
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}-${
|
|
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
|
|
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
|
|
11404
|
+
const profiles3 = await service.listDeploymentProfiles({
|
|
10288
11405
|
unactivated: opts.unactivated
|
|
10289
11406
|
});
|
|
10290
|
-
renderDeploymentProfileList(
|
|
11407
|
+
renderDeploymentProfileList(profiles3, fmt);
|
|
10291
11408
|
} catch (err) {
|
|
10292
11409
|
fail(err);
|
|
10293
11410
|
}
|
|
10294
11411
|
});
|
|
10295
|
-
const
|
|
10296
|
-
|
|
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 =
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
10645
|
-
registerApplyCommand(
|
|
10646
|
-
registerCreateCommand(
|
|
10647
|
-
|
|
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(
|
|
10666
|
-
const topicsGet =
|
|
11783
|
+
registerEvalCommand(topics2);
|
|
11784
|
+
const topicsGet = topics2.command("get <nameOrId>").description("Get a custom topic by name or UUID").option("--revision <n>", "Select an exact revision", Number).option("--all-versions", "Return every matching revision").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (nameOrId, opts) => {
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
10712
|
-
registerSampleCommand(
|
|
10713
|
-
|
|
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
|
|
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()}-${
|
|
12040
|
+
`debug-api-${Date.now()}-${randomUUID10().slice(0, 8)}.jsonl`
|
|
10807
12041
|
);
|
|
10808
12042
|
try {
|
|
10809
|
-
|
|
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);
|