@cdot65/prisma-airs-cli 5.2.0 → 5.4.1
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 +5 -5
- package/dist/{chunk-7Z76VCB3.js → chunk-36Z6IPH6.js} +18 -2
- package/dist/cli/index.js +932 -448
- package/dist/index.d.ts +16 -2
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
validateTopic,
|
|
31
31
|
writeBackupFile,
|
|
32
32
|
writeReportFile
|
|
33
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-36Z6IPH6.js";
|
|
34
34
|
|
|
35
35
|
// src/cli/index.ts
|
|
36
36
|
import "dotenv/config";
|
|
@@ -49,189 +49,36 @@ function installProcessGuards() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/cli/program.ts
|
|
52
|
-
import { randomUUID as
|
|
52
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
53
53
|
import { readFileSync as readFileSync4 } from "fs";
|
|
54
54
|
import { dirname as dirname4, join as join4 } from "path";
|
|
55
55
|
import { fileURLToPath } from "url";
|
|
56
56
|
import { Command } from "commander";
|
|
57
57
|
|
|
58
|
-
// src/cli/commands/
|
|
58
|
+
// src/cli/commands/agentguard.ts
|
|
59
|
+
import { randomUUID } from "crypto";
|
|
60
|
+
import { lstat } from "fs/promises";
|
|
61
|
+
import { resolve } from "path";
|
|
59
62
|
import {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
AgentGuardClient,
|
|
64
|
+
AgentGuardPageOptionsSchema,
|
|
65
|
+
AgentGuardScanListOptionsSchema,
|
|
66
|
+
AgentGuardStatsOptionsSchema
|
|
63
67
|
} from "@cdot65/prisma-airs-sdk";
|
|
68
|
+
import { dump as dump3 } from "js-yaml";
|
|
69
|
+
import { z } from "zod";
|
|
64
70
|
|
|
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) {
|
|
71
|
+
// src/config/client-options.ts
|
|
72
|
+
function agentGuardClientOptions(config) {
|
|
90
73
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
settings: raw.settings
|
|
74
|
+
clientId: config.mgmtClientId,
|
|
75
|
+
clientSecret: config.mgmtClientSecret,
|
|
76
|
+
tsgId: config.mgmtTsgId,
|
|
77
|
+
dataEndpoint: config.agentGuardDataEndpoint,
|
|
78
|
+
mgmtEndpoint: config.agentGuardMgmtEndpoint,
|
|
79
|
+
tokenEndpoint: config.agentGuardTokenEndpoint ?? config.mgmtTokenEndpoint
|
|
98
80
|
};
|
|
99
81
|
}
|
|
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
82
|
function managementClientOptions(config) {
|
|
236
83
|
return {
|
|
237
84
|
clientId: config.mgmtClientId,
|
|
@@ -283,6 +130,207 @@ function modelSecurityClientOptions(config) {
|
|
|
283
130
|
};
|
|
284
131
|
}
|
|
285
132
|
|
|
133
|
+
// src/reports/agentguard.ts
|
|
134
|
+
async function collectAgentGuardReport(client, options = {}) {
|
|
135
|
+
const maxPages = options.maxPages ?? 40;
|
|
136
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100)
|
|
137
|
+
throw new Error("maxPages must be between 1 and 100");
|
|
138
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
139
|
+
const end = now();
|
|
140
|
+
const window2 = {
|
|
141
|
+
start: new Date(end.getTime() - 30 * 864e5).toISOString(),
|
|
142
|
+
end: end.toISOString()
|
|
143
|
+
};
|
|
144
|
+
const report = {
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
product: "Prisma AIRS AI Supply Chain \u2014 AgentGuard",
|
|
147
|
+
title: options.title ?? "AgentGuard environment report",
|
|
148
|
+
collectionStartedAt: end.toISOString(),
|
|
149
|
+
generatedAt: "",
|
|
150
|
+
window: window2,
|
|
151
|
+
windowLabel: "30-day scan inventory; rolling 30_DAYS statistics; current rule catalog",
|
|
152
|
+
schemaLabel: "Experimental browser APIs",
|
|
153
|
+
health: "unknown",
|
|
154
|
+
sources: [],
|
|
155
|
+
findings: [],
|
|
156
|
+
tables: [],
|
|
157
|
+
limitations: [
|
|
158
|
+
"Read-only evidence, not a security certification or numerical health score. Browser APIs are undocumented and may change.",
|
|
159
|
+
"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.",
|
|
160
|
+
"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.",
|
|
161
|
+
"Rule default states describe the catalog, not effective tenant policies. No scan creation, policy changes or skill uploads are performed.",
|
|
162
|
+
"Missing or incomplete evidence is unknown, not zero. Scan names, identifiers, URLs, device metadata, source code and finding text are excluded."
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
const label = (v, allowed) => v === null ? "UNKNOWN" : allowed.includes(v) ? v : "OTHER";
|
|
166
|
+
async function source(name, method, range, read) {
|
|
167
|
+
const s = {
|
|
168
|
+
name,
|
|
169
|
+
method,
|
|
170
|
+
window: range,
|
|
171
|
+
status: "unavailable",
|
|
172
|
+
pages: 0,
|
|
173
|
+
records: 0,
|
|
174
|
+
notes: []
|
|
175
|
+
};
|
|
176
|
+
report.sources.push(s);
|
|
177
|
+
try {
|
|
178
|
+
await read(s);
|
|
179
|
+
if (s.status !== "partial") s.status = "complete";
|
|
180
|
+
} catch {
|
|
181
|
+
s.status = s.records ? "partial" : "unavailable";
|
|
182
|
+
s.notes.push("Request or validation failed; no upstream error body included.");
|
|
183
|
+
}
|
|
184
|
+
if (s.status !== "complete")
|
|
185
|
+
report.findings.push({
|
|
186
|
+
priority: "review",
|
|
187
|
+
title: `${name} incomplete`,
|
|
188
|
+
evidence: s.notes.join(" "),
|
|
189
|
+
recommendation: "Check service access or increase the page budget, then regenerate.",
|
|
190
|
+
source: name
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function pages(s, read, pageCount = false) {
|
|
194
|
+
const rows = [];
|
|
195
|
+
const seen = /* @__PURE__ */ new Set();
|
|
196
|
+
let total;
|
|
197
|
+
try {
|
|
198
|
+
for (let page = 0; page < maxPages; page++) {
|
|
199
|
+
const r = await read(rows.length);
|
|
200
|
+
s.pages++;
|
|
201
|
+
if (!Number.isSafeInteger(r.total) || r.total < 0 || !Array.isArray(r.rows) || r.rows.length > 10)
|
|
202
|
+
throw new Error();
|
|
203
|
+
total ??= r.total;
|
|
204
|
+
if (!pageCount && (r.total !== total || rows.length + r.rows.length > total))
|
|
205
|
+
throw new Error();
|
|
206
|
+
for (const row of r.rows) {
|
|
207
|
+
if (!row.uuid || seen.has(row.uuid)) throw new Error();
|
|
208
|
+
seen.add(row.uuid);
|
|
209
|
+
rows.push(row);
|
|
210
|
+
s.records = rows.length;
|
|
211
|
+
}
|
|
212
|
+
if (pageCount ? r.rows.length < 10 : rows.length === total) return rows;
|
|
213
|
+
if (!r.rows.length) throw new Error();
|
|
214
|
+
}
|
|
215
|
+
s.notes.push(`Reached the ${maxPages}-page budget.`);
|
|
216
|
+
throw new Error();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (!rows.length) throw error;
|
|
219
|
+
s.status = "partial";
|
|
220
|
+
s.notes.push(
|
|
221
|
+
"Inventory incomplete: pagination changed, repeated, stopped early, failed or reached its budget."
|
|
222
|
+
);
|
|
223
|
+
return rows;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await source("Scans", "listScans()", `${window2.start} to ${window2.end}`, async (s) => {
|
|
227
|
+
const rows = await pages(s, async (skip) => {
|
|
228
|
+
const p = await client.listScans({
|
|
229
|
+
skip,
|
|
230
|
+
limit: 10,
|
|
231
|
+
start_time: window2.start,
|
|
232
|
+
end_time: window2.end,
|
|
233
|
+
isBackgroundRefresh: false
|
|
234
|
+
});
|
|
235
|
+
return { rows: p.scans, total: p.pagination.total_items };
|
|
236
|
+
});
|
|
237
|
+
const inside = rows.filter((r) => {
|
|
238
|
+
const t = Date.parse(r.created_at);
|
|
239
|
+
return Number.isFinite(t) && t >= Date.parse(window2.start) && t <= end.getTime();
|
|
240
|
+
});
|
|
241
|
+
if (inside.length !== rows.length) {
|
|
242
|
+
s.status = "partial";
|
|
243
|
+
s.notes.push("Rows with missing or out-of-window creation timestamps were excluded.");
|
|
244
|
+
}
|
|
245
|
+
const leaf = inside.filter((r) => !r.is_batch && r.child_scan_uuids.length === 0);
|
|
246
|
+
const groups = /* @__PURE__ */ new Map();
|
|
247
|
+
for (const r of leaf) {
|
|
248
|
+
const key = `${label(r.artifact_type, ["SKILL", "AGENT"])} / ${label(r.eval_outcome, ["BLOCKED", "ALLOWED", "PENDING", "ERROR"])}`;
|
|
249
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
250
|
+
}
|
|
251
|
+
report.tables.push({
|
|
252
|
+
title: "Scan outcomes",
|
|
253
|
+
note: `Collected ${s.records} rows; excluded ${inside.length - leaf.length} parent/batch rows. ${s.status === "partial" ? "Partial inventory only." : "Complete inventory for requested window."}`,
|
|
254
|
+
headers: ["Artifact / outcome", "Scan rows"],
|
|
255
|
+
rows: [...groups].sort(([a], [b]) => a.localeCompare(b))
|
|
256
|
+
});
|
|
257
|
+
const blocked = leaf.filter((r) => r.eval_outcome === "BLOCKED").length;
|
|
258
|
+
if (blocked)
|
|
259
|
+
report.findings.push({
|
|
260
|
+
priority: "attention",
|
|
261
|
+
title: "Blocked scans require review",
|
|
262
|
+
evidence: `${blocked} collected non-parent scan rows have BLOCKED evaluations.`,
|
|
263
|
+
recommendation: "Inspect the affected scans and findings before approving the artifacts.",
|
|
264
|
+
source: "Scans"
|
|
265
|
+
});
|
|
266
|
+
const unsettled = leaf.filter(
|
|
267
|
+
(r) => r.status !== "COMPLETED" || !["ALLOWED", "BLOCKED"].includes(r.eval_outcome ?? "")
|
|
268
|
+
).length;
|
|
269
|
+
if (unsettled)
|
|
270
|
+
report.findings.push({
|
|
271
|
+
priority: "review",
|
|
272
|
+
title: "Scans without a completed decision",
|
|
273
|
+
evidence: `${unsettled} collected non-parent rows are pending, failed or have an unfamiliar status.`,
|
|
274
|
+
recommendation: "Review scan status; absence of a completed decision is not a passing evaluation.",
|
|
275
|
+
source: "Scans"
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
await source("Statistics", "getScanStats()", "Server rolling 30_DAYS", async (s) => {
|
|
279
|
+
const r = await client.getScanStats({ time_period: "30_DAYS" });
|
|
280
|
+
report.tables.push({
|
|
281
|
+
title: "Skill statistics",
|
|
282
|
+
note: "Server aggregates; percentage changes may be unavailable.",
|
|
283
|
+
headers: ["Metric", "Count", "Change (%)"],
|
|
284
|
+
rows: [
|
|
285
|
+
[
|
|
286
|
+
"Unique skills scanned",
|
|
287
|
+
r.unique_skills_scanned.count,
|
|
288
|
+
r.unique_skills_scanned.percent_change
|
|
289
|
+
],
|
|
290
|
+
[
|
|
291
|
+
"Vulnerabilities found",
|
|
292
|
+
r.total_vulnerabilities_found.count,
|
|
293
|
+
r.total_vulnerabilities_found.percent_change
|
|
294
|
+
]
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
if (r.total_vulnerabilities_found.count > 0)
|
|
298
|
+
report.findings.push({
|
|
299
|
+
priority: "attention",
|
|
300
|
+
title: "Skill vulnerabilities reported",
|
|
301
|
+
evidence: `${r.total_vulnerabilities_found.count} findings reported in the server statistics window.`,
|
|
302
|
+
recommendation: "Review vulnerability details and remediate before use; findings are not a count of distinct vulnerable skills.",
|
|
303
|
+
source: "Statistics"
|
|
304
|
+
});
|
|
305
|
+
s.records = 1;
|
|
306
|
+
s.pages = 1;
|
|
307
|
+
});
|
|
308
|
+
await source("Rules", "listRules()", "Current catalog", async (s) => {
|
|
309
|
+
const rows = await pages(
|
|
310
|
+
s,
|
|
311
|
+
async (skip) => {
|
|
312
|
+
const p = await client.listRules({ skip, limit: 10 });
|
|
313
|
+
return { rows: p.rules, total: p.pagination.total_items };
|
|
314
|
+
},
|
|
315
|
+
true
|
|
316
|
+
);
|
|
317
|
+
const groups = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const r of rows) {
|
|
319
|
+
const state = label(r.default_state, ["BLOCKING", "ALLOWING"]);
|
|
320
|
+
groups.set(state, (groups.get(state) ?? 0) + 1);
|
|
321
|
+
}
|
|
322
|
+
report.tables.push({
|
|
323
|
+
title: "Rule catalog defaults",
|
|
324
|
+
note: "These are not effective tenant policy settings.",
|
|
325
|
+
headers: ["Default state", "Rules"],
|
|
326
|
+
rows: [...groups].sort(([a], [b]) => a.localeCompare(b))
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
report.health = report.sources.every((s) => s.status === "unavailable") ? "unknown" : report.findings.some((f) => f.priority === "attention") ? "attention" : report.findings.length ? "review" : "no-findings";
|
|
330
|
+
report.generatedAt = now().toISOString();
|
|
331
|
+
return report;
|
|
332
|
+
}
|
|
333
|
+
|
|
286
334
|
// src/cli/renderer/aigateway.ts
|
|
287
335
|
import chalk4 from "chalk";
|
|
288
336
|
import { dump as yamlDump } from "js-yaml";
|
|
@@ -2787,140 +2835,541 @@ function renderCustomerAppList(apps, format = "pretty") {
|
|
|
2787
2835
|
);
|
|
2788
2836
|
return;
|
|
2789
2837
|
}
|
|
2790
|
-
ui.section("Customer Apps:");
|
|
2791
|
-
for (const a of apps) {
|
|
2792
|
-
if (a.id) ui.dim(a.id);
|
|
2793
|
-
const desc = a.description ? chalk9.dim(` \u2014 ${a.description.slice(0, 80)}`) : "";
|
|
2794
|
-
console.log(` ${a.name}${desc}`);
|
|
2838
|
+
ui.section("Customer Apps:");
|
|
2839
|
+
for (const a of apps) {
|
|
2840
|
+
if (a.id) ui.dim(a.id);
|
|
2841
|
+
const desc = a.description ? chalk9.dim(` \u2014 ${a.description.slice(0, 80)}`) : "";
|
|
2842
|
+
console.log(` ${a.name}${desc}`);
|
|
2843
|
+
}
|
|
2844
|
+
console.log();
|
|
2845
|
+
}
|
|
2846
|
+
function renderCustomerAppDetail(app) {
|
|
2847
|
+
ui.section("Customer App Detail:");
|
|
2848
|
+
const pairs = [];
|
|
2849
|
+
if (app.id) pairs.push(["ID", app.id]);
|
|
2850
|
+
pairs.push(["Name", app.name]);
|
|
2851
|
+
if (app.description) pairs.push(["Desc", app.description]);
|
|
2852
|
+
pairs.push(["Data", JSON.stringify(app.raw, null, 2).slice(0, 500)]);
|
|
2853
|
+
ui.keyValue(pairs);
|
|
2854
|
+
console.log();
|
|
2855
|
+
}
|
|
2856
|
+
function renderCustomerAppConsumption(data, format = "pretty") {
|
|
2857
|
+
const fmt = (n, scale) => n == null ? "-" : `${n}${scale ?? ""}`;
|
|
2858
|
+
if (format !== "pretty") {
|
|
2859
|
+
const rows = data.detectors.map((d) => ({
|
|
2860
|
+
app_name: data.appName,
|
|
2861
|
+
app_id: data.appId,
|
|
2862
|
+
monitoring_since: data.monitoringSince ?? "",
|
|
2863
|
+
daily_avg: fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale),
|
|
2864
|
+
monthly_total: fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale),
|
|
2865
|
+
sessions_total: data.sessions.total,
|
|
2866
|
+
sessions_violating: data.sessions.violating,
|
|
2867
|
+
detector: d.type,
|
|
2868
|
+
critical: d.critical,
|
|
2869
|
+
high: d.high,
|
|
2870
|
+
medium: d.medium,
|
|
2871
|
+
low: d.low,
|
|
2872
|
+
total: d.total
|
|
2873
|
+
}));
|
|
2874
|
+
console.log(
|
|
2875
|
+
formatOutput(
|
|
2876
|
+
rows,
|
|
2877
|
+
[
|
|
2878
|
+
{ key: "app_name", label: "App" },
|
|
2879
|
+
{ key: "app_id", label: "AppId" },
|
|
2880
|
+
{ key: "monitoring_since", label: "MonitoringSince" },
|
|
2881
|
+
{ key: "daily_avg", label: "DailyAvg" },
|
|
2882
|
+
{ key: "monthly_total", label: "MonthlyTotal" },
|
|
2883
|
+
{ key: "sessions_total", label: "Sessions" },
|
|
2884
|
+
{ key: "sessions_violating", label: "Violating" },
|
|
2885
|
+
{ key: "detector", label: "Detector" },
|
|
2886
|
+
{ key: "critical", label: "C" },
|
|
2887
|
+
{ key: "high", label: "H" },
|
|
2888
|
+
{ key: "medium", label: "M" },
|
|
2889
|
+
{ key: "low", label: "L" },
|
|
2890
|
+
{ key: "total", label: "Total" }
|
|
2891
|
+
],
|
|
2892
|
+
format
|
|
2893
|
+
)
|
|
2894
|
+
);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
ui.header(data.appName, `(${data.appId})`);
|
|
2898
|
+
const pairs = [];
|
|
2899
|
+
if (data.monitoringSince) pairs.push(["Monitoring since", data.monitoringSince]);
|
|
2900
|
+
if (data.source) pairs.push(["Source", data.source]);
|
|
2901
|
+
if (data.cloud) pairs.push(["Cloud", data.cloud]);
|
|
2902
|
+
if (data.profiles.length > 0) pairs.push(["Profiles", data.profiles.join(", ")]);
|
|
2903
|
+
if (pairs.length > 0) ui.keyValue(pairs);
|
|
2904
|
+
ui.section("Token consumption:");
|
|
2905
|
+
ui.keyValue([
|
|
2906
|
+
["Daily avg", fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale)],
|
|
2907
|
+
["Monthly total", fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale)]
|
|
2908
|
+
]);
|
|
2909
|
+
ui.section("Sessions:");
|
|
2910
|
+
ui.keyValue([
|
|
2911
|
+
["Total", data.sessions.total],
|
|
2912
|
+
["Violating", data.sessions.violating]
|
|
2913
|
+
]);
|
|
2914
|
+
const firing = data.detectors.filter((d) => d.total > 0);
|
|
2915
|
+
ui.section(
|
|
2916
|
+
`Detectors (${data.totalViolating} violating, ${firing.length}/${data.detectors.length} firing):`
|
|
2917
|
+
);
|
|
2918
|
+
if (firing.length === 0) {
|
|
2919
|
+
ui.dim("no detector violations in window");
|
|
2920
|
+
} else {
|
|
2921
|
+
ui.table(
|
|
2922
|
+
[
|
|
2923
|
+
{ key: "detector", label: "Detector" },
|
|
2924
|
+
{ key: "total", label: "Total" },
|
|
2925
|
+
{ key: "severity", label: "Severity" }
|
|
2926
|
+
],
|
|
2927
|
+
firing.map((d) => ({
|
|
2928
|
+
detector: d.type,
|
|
2929
|
+
total: d.total,
|
|
2930
|
+
severity: `c=${d.critical} h=${d.high} m=${d.medium} l=${d.low}`
|
|
2931
|
+
}))
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
console.log();
|
|
2935
|
+
}
|
|
2936
|
+
function renderDeploymentProfileList(profiles, format = "pretty") {
|
|
2937
|
+
if (profiles.length === 0) {
|
|
2938
|
+
ui.emptyList("deployment profiles");
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (format !== "pretty") {
|
|
2942
|
+
const rows = profiles.map((p) => ({
|
|
2943
|
+
name: p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "",
|
|
2944
|
+
status: p.raw.status ?? "",
|
|
2945
|
+
authCode: p.raw.auth_code ?? ""
|
|
2946
|
+
}));
|
|
2947
|
+
console.log(
|
|
2948
|
+
formatOutput(
|
|
2949
|
+
rows,
|
|
2950
|
+
[
|
|
2951
|
+
{ key: "name", label: "Name" },
|
|
2952
|
+
{ key: "status", label: "Status" },
|
|
2953
|
+
{ key: "authCode", label: "Auth Code" }
|
|
2954
|
+
],
|
|
2955
|
+
format
|
|
2956
|
+
)
|
|
2957
|
+
);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
ui.section("Deployment Profiles:");
|
|
2961
|
+
for (const p of profiles) {
|
|
2962
|
+
const name = p.raw.dp_name ?? p.raw.profile_name ?? p.raw.name ?? "unknown";
|
|
2963
|
+
const status = p.raw.status;
|
|
2964
|
+
const authCode = p.raw.auth_code;
|
|
2965
|
+
const statusColor5 = status === "active" ? chalk9.green : chalk9.dim;
|
|
2966
|
+
console.log(
|
|
2967
|
+
` ${name}${status ? ` ${statusColor5(status)}` : ""}${authCode ? ` ${chalk9.dim(authCode)}` : ""}`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
console.log();
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// src/cli/commands/agentguard.ts
|
|
2974
|
+
var createClient = async () => new AgentGuardClient({ ...agentGuardClientOptions(await loadConfig()), numRetries: 0 });
|
|
2975
|
+
var integer = (value) => {
|
|
2976
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
2977
|
+
usageError("Pagination requires a safe nonnegative integer");
|
|
2978
|
+
return Number(value);
|
|
2979
|
+
};
|
|
2980
|
+
var pageOptions = (cmd) => cmd.option("--limit <n>", "Page size", integer, 10).option("--offset <n>", "Starting offset", integer, 0);
|
|
2981
|
+
var readOutput = (cmd) => cmd.option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml");
|
|
2982
|
+
var query = (schema, value) => {
|
|
2983
|
+
const result = schema.safeParse(value);
|
|
2984
|
+
if (!result.success)
|
|
2985
|
+
usageError(
|
|
2986
|
+
"Invalid AgentGuard query. Check pagination, UUID and ISO timestamps; statistics support 30_DAYS only."
|
|
2987
|
+
);
|
|
2988
|
+
return result.data;
|
|
2989
|
+
};
|
|
2990
|
+
function emit(value, format) {
|
|
2991
|
+
if (format === "json") console.log(JSON.stringify(value, null, 2));
|
|
2992
|
+
else if (format === "yaml") console.log(dump3(value, { noRefs: true, lineWidth: -1 }).trimEnd());
|
|
2993
|
+
else {
|
|
2994
|
+
const records = Object.values(value).find(Array.isArray);
|
|
2995
|
+
if (records) {
|
|
2996
|
+
const keys = [...new Set(records.flatMap((r) => Object.keys(r)))].filter((k) => !["description", "remediation"].includes(k)).slice(0, 9);
|
|
2997
|
+
console.log(
|
|
2998
|
+
formatOutput(
|
|
2999
|
+
records,
|
|
3000
|
+
keys.map((key) => ({ key, label: key.replaceAll("_", " ") })),
|
|
3001
|
+
format === "pretty" ? "table" : format
|
|
3002
|
+
)
|
|
3003
|
+
);
|
|
3004
|
+
ui.status(`Returned ${records.length} records; see JSON/YAML for pagination metadata.`);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
const rows = Object.entries(value).map(([key, v]) => ({
|
|
3008
|
+
key,
|
|
3009
|
+
value: typeof v === "object" ? JSON.stringify(v) : String(v)
|
|
3010
|
+
}));
|
|
3011
|
+
console.log(
|
|
3012
|
+
formatOutput(
|
|
3013
|
+
rows,
|
|
3014
|
+
[
|
|
3015
|
+
{ key: "key", label: "Field" },
|
|
3016
|
+
{ key: "value", label: "Value" }
|
|
3017
|
+
],
|
|
3018
|
+
format === "pretty" ? "table" : format
|
|
3019
|
+
)
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
async function run(read, cmd) {
|
|
3024
|
+
try {
|
|
3025
|
+
const format = await resolveOutput(cmd, cmd.opts());
|
|
3026
|
+
emit(await read(), format);
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
const status = error?.statusCode;
|
|
3029
|
+
ui.error(
|
|
3030
|
+
`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.`
|
|
3031
|
+
);
|
|
3032
|
+
process.exitCode = 1;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
async function inventory(read, options, pageCount = false) {
|
|
3036
|
+
if (options.max !== void 0 && (!Number.isSafeInteger(options.max) || options.max < 0))
|
|
3037
|
+
usageError("--max must be a safe nonnegative integer");
|
|
3038
|
+
const rows = [];
|
|
3039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3040
|
+
let total;
|
|
3041
|
+
const cap = options.all ? options.max || Number.MAX_SAFE_INTEGER : options.limit;
|
|
3042
|
+
for (let page = 0; page < 1e3; page++) {
|
|
3043
|
+
const limit = Math.min(options.limit, cap - rows.length);
|
|
3044
|
+
const p = await read(options.offset + rows.length, limit);
|
|
3045
|
+
total ??= p.total;
|
|
3046
|
+
if (!pageCount && p.total !== total || p.rows.length > limit)
|
|
3047
|
+
throw new Error("Unstable pagination");
|
|
3048
|
+
for (const row of p.rows) {
|
|
3049
|
+
if (!row.uuid || seen.has(row.uuid)) throw new Error("Repeated identity");
|
|
3050
|
+
seen.add(row.uuid);
|
|
3051
|
+
rows.push(row);
|
|
3052
|
+
}
|
|
3053
|
+
const next = options.offset + rows.length;
|
|
3054
|
+
if (!pageCount && next > total && rows.length) throw new Error("Invalid total");
|
|
3055
|
+
const complete = pageCount ? p.rows.length < limit : next >= total;
|
|
3056
|
+
if (!options.all || complete || rows.length === cap)
|
|
3057
|
+
return {
|
|
3058
|
+
rows,
|
|
3059
|
+
pagination: {
|
|
3060
|
+
total_items: pageCount ? complete && options.offset === 0 ? rows.length : null : total,
|
|
3061
|
+
returned: rows.length,
|
|
3062
|
+
skip: options.offset,
|
|
3063
|
+
next_offset: complete ? null : next,
|
|
3064
|
+
truncated: !complete
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
if (!p.rows.length) throw new Error("Premature empty page");
|
|
3068
|
+
}
|
|
3069
|
+
throw new Error("Pagination safety limit reached");
|
|
3070
|
+
}
|
|
3071
|
+
function registerAgentGuardCommand(program) {
|
|
3072
|
+
const group = program.command("agentguard").description("AI Supply Chain agent and skill scan telemetry (experimental, OAuth)");
|
|
3073
|
+
const scans = group.command("scans").description("Inspect agent and skill scans (read-only)");
|
|
3074
|
+
const list = readOutput(
|
|
3075
|
+
pageOptions(
|
|
3076
|
+
scans.command("list").description("List one page of scans; sensitive metadata is excluded")
|
|
3077
|
+
)
|
|
3078
|
+
).option("--start <iso>", "Inclusive creation time start (ISO timestamp)").option("--end <iso>", "Creation time end (ISO timestamp)").action(async (opts) => {
|
|
3079
|
+
const options = query(AgentGuardScanListOptionsSchema, {
|
|
3080
|
+
skip: opts.offset,
|
|
3081
|
+
limit: opts.limit,
|
|
3082
|
+
start_time: opts.start,
|
|
3083
|
+
end_time: opts.end,
|
|
3084
|
+
isBackgroundRefresh: false
|
|
3085
|
+
});
|
|
3086
|
+
await run(async () => {
|
|
3087
|
+
const client = await createClient();
|
|
3088
|
+
const p = await inventory(async (skip, limit) => {
|
|
3089
|
+
const page = await client.listScans({ ...options, skip, limit });
|
|
3090
|
+
return { rows: page.scans, total: page.pagination.total_items };
|
|
3091
|
+
}, opts);
|
|
3092
|
+
return {
|
|
3093
|
+
pagination: p.pagination,
|
|
3094
|
+
scans: p.rows.map((r) => ({
|
|
3095
|
+
uuid: r.uuid,
|
|
3096
|
+
artifact_type: r.artifact_type,
|
|
3097
|
+
status: r.status,
|
|
3098
|
+
eval_outcome: r.eval_outcome,
|
|
3099
|
+
created_at: r.created_at,
|
|
3100
|
+
vulnerability_count: r.summary?.vulnerability_count ?? null,
|
|
3101
|
+
attack_chain_count: r.summary?.attack_chain_count ?? null,
|
|
3102
|
+
is_batch: r.is_batch,
|
|
3103
|
+
parent_uuid: r.parent_uuid
|
|
3104
|
+
}))
|
|
3105
|
+
};
|
|
3106
|
+
}, list);
|
|
3107
|
+
});
|
|
3108
|
+
const vulnerabilities = readOutput(
|
|
3109
|
+
scans.command("vulnerabilities <scanUuid>").description("List finding metadata; --include-content explicitly reveals sensitive details")
|
|
3110
|
+
).option("--include-content", "Include source code, paths and finding text (sensitive)").action(async (id, opts) => {
|
|
3111
|
+
query(z.string().uuid(), id);
|
|
3112
|
+
await run(async () => {
|
|
3113
|
+
const p = await (await createClient()).listScanVulnerabilities(id);
|
|
3114
|
+
return {
|
|
3115
|
+
pagination: p.pagination,
|
|
3116
|
+
vulnerabilities: p.vulnerabilities.map(
|
|
3117
|
+
(r) => opts.includeContent ? r : {
|
|
3118
|
+
uuid: r.uuid,
|
|
3119
|
+
scan_uuid: r.scan_uuid,
|
|
3120
|
+
vulnerability_type: r.vulnerability_type,
|
|
3121
|
+
attack_chain_count: r.attack_chain_count
|
|
3122
|
+
}
|
|
3123
|
+
),
|
|
3124
|
+
content_included: !!opts.includeContent
|
|
3125
|
+
};
|
|
3126
|
+
}, vulnerabilities);
|
|
3127
|
+
});
|
|
3128
|
+
const stats = readOutput(
|
|
3129
|
+
group.command("stats").description("Read server rolling 30-day skill statistics")
|
|
3130
|
+
).option("--time-period <period>", "Verified period: 30_DAYS", "30_DAYS").action(async (opts) => {
|
|
3131
|
+
const options = query(AgentGuardStatsOptionsSchema, { time_period: opts.timePeriod });
|
|
3132
|
+
await run(async () => (await createClient()).getScanStats(options), stats);
|
|
3133
|
+
});
|
|
3134
|
+
const rulesGroup = group.command("rules").description("Rule catalog (not effective tenant policy)");
|
|
3135
|
+
const rules = readOutput(
|
|
3136
|
+
pageOptions(rulesGroup.command("list").description("List rule catalog defaults"))
|
|
3137
|
+
).action(async (opts) => {
|
|
3138
|
+
const options = query(AgentGuardPageOptionsSchema, { skip: opts.offset, limit: opts.limit });
|
|
3139
|
+
await run(async () => {
|
|
3140
|
+
const client = await createClient();
|
|
3141
|
+
const p = await inventory(
|
|
3142
|
+
async (skip, limit) => {
|
|
3143
|
+
const page = await client.listRules({ ...options, skip, limit });
|
|
3144
|
+
return { rows: page.rules, total: page.pagination.total_items };
|
|
3145
|
+
},
|
|
3146
|
+
opts,
|
|
3147
|
+
true
|
|
3148
|
+
);
|
|
3149
|
+
return { pagination: p.pagination, rules: p.rows };
|
|
3150
|
+
}, rules);
|
|
3151
|
+
});
|
|
3152
|
+
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) => {
|
|
3153
|
+
const format = opts.output ?? program.opts().output ?? "html";
|
|
3154
|
+
if (!["html", "markdown"].includes(format))
|
|
3155
|
+
usageError("Report output must be html or markdown");
|
|
3156
|
+
if (opts.maxPages < 1 || opts.maxPages > 100)
|
|
3157
|
+
usageError("--max-pages must be between 1 and 100");
|
|
3158
|
+
if (!opts.title.trim() || opts.title.length > 240)
|
|
3159
|
+
usageError("--title must contain 1\u2013240 characters");
|
|
3160
|
+
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
3161
|
+
usageError("--output-file cannot be empty");
|
|
3162
|
+
const path3 = opts.outputFile === "-" ? void 0 : resolve(
|
|
3163
|
+
opts.outputFile ?? `airs-agentguard-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID().slice(0, 8)}.${format === "html" ? "html" : "md"}`
|
|
3164
|
+
);
|
|
3165
|
+
try {
|
|
3166
|
+
if (path3) {
|
|
3167
|
+
const exists = await lstat(path3).catch((e) => {
|
|
3168
|
+
if (e.code !== "ENOENT") throw e;
|
|
3169
|
+
return void 0;
|
|
3170
|
+
});
|
|
3171
|
+
if (exists) throw new Error();
|
|
3172
|
+
}
|
|
3173
|
+
const data = await collectAgentGuardReport(await createClient(), {
|
|
3174
|
+
title: opts.title,
|
|
3175
|
+
maxPages: opts.maxPages
|
|
3176
|
+
});
|
|
3177
|
+
const output = format === "html" ? renderEnvironmentReportHtml(data) : renderEnvironmentReportMarkdown(data);
|
|
3178
|
+
if (path3) {
|
|
3179
|
+
await writeReportFile(path3, output);
|
|
3180
|
+
ui.status(`Report \u2192 ${path3.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
|
|
3181
|
+
} else
|
|
3182
|
+
await new Promise(
|
|
3183
|
+
(done, reject) => process.stdout.write(output, (error) => error ? reject(error) : done())
|
|
3184
|
+
);
|
|
3185
|
+
const incomplete = data.sources.filter((s) => s.status !== "complete").length;
|
|
3186
|
+
if (incomplete)
|
|
3187
|
+
ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
|
|
3188
|
+
if (data.health === "unknown" || opts.strict && incomplete) process.exitCode = 1;
|
|
3189
|
+
} catch {
|
|
3190
|
+
ui.error(
|
|
3191
|
+
"AgentGuard report failed. Check credentials and a writable, new output path. Existing files are never overwritten."
|
|
3192
|
+
);
|
|
3193
|
+
process.exitCode = 1;
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// src/cli/commands/aigateway.ts
|
|
3199
|
+
import {
|
|
3200
|
+
GatewayDefaultsInputSchema,
|
|
3201
|
+
GatewayRateLimitInputSchema,
|
|
3202
|
+
GatewayUsageLimitInputSchema
|
|
3203
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
3204
|
+
|
|
3205
|
+
// src/airs/aigateway.ts
|
|
3206
|
+
import {
|
|
3207
|
+
AIGatewayChartFiltersSchema,
|
|
3208
|
+
AIGatewayClient
|
|
3209
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
3210
|
+
function toLimitArray(value) {
|
|
3211
|
+
if (Array.isArray(value)) return value;
|
|
3212
|
+
if (value !== null && typeof value === "object") return [value];
|
|
3213
|
+
return [];
|
|
3214
|
+
}
|
|
3215
|
+
function normalizeWorkspace(raw) {
|
|
3216
|
+
return {
|
|
3217
|
+
id: raw.id,
|
|
3218
|
+
slug: raw.slug,
|
|
3219
|
+
name: raw.name,
|
|
3220
|
+
icon: raw.icon,
|
|
3221
|
+
description: raw.description,
|
|
3222
|
+
createdAt: raw.created_at,
|
|
3223
|
+
lastUpdatedAt: raw.last_updated_at,
|
|
3224
|
+
isDefault: Boolean(raw.is_default),
|
|
3225
|
+
status: raw.status,
|
|
3226
|
+
scopeName: raw.scope_name
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
function normalizeWorkspaceDetail(raw) {
|
|
3230
|
+
return {
|
|
3231
|
+
...normalizeWorkspace(raw),
|
|
3232
|
+
defaults: raw.defaults,
|
|
3233
|
+
usageLimits: toLimitArray(raw.usage_limits),
|
|
3234
|
+
rateLimits: toLimitArray(raw.rate_limits),
|
|
3235
|
+
securitySettings: raw.security_settings,
|
|
3236
|
+
dataPlaneSecuritySettings: raw.data_plane_security_settings,
|
|
3237
|
+
settings: raw.settings
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
function aiGatewayGrantHint(err) {
|
|
3241
|
+
const status = err?.status ?? err?.statusCode;
|
|
3242
|
+
if (status !== 403) return void 0;
|
|
3243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3244
|
+
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)";
|
|
3245
|
+
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.`;
|
|
3246
|
+
}
|
|
3247
|
+
var SdkAiGatewayService = class {
|
|
3248
|
+
client;
|
|
3249
|
+
constructor(opts) {
|
|
3250
|
+
this.client = new AIGatewayClient(opts);
|
|
3251
|
+
}
|
|
3252
|
+
async listWorkspaces(options) {
|
|
3253
|
+
const response = await this.client.workspaces.list(options);
|
|
3254
|
+
return response.data.map(normalizeWorkspace);
|
|
3255
|
+
}
|
|
3256
|
+
async listAllWorkspaces() {
|
|
3257
|
+
const [active, archived] = await Promise.all([
|
|
3258
|
+
this.client.workspaces.list({ plane: "admin" }),
|
|
3259
|
+
this.client.workspaces.list({ plane: "admin", status: "archived" })
|
|
3260
|
+
]);
|
|
3261
|
+
return [
|
|
3262
|
+
...active.data,
|
|
3263
|
+
...archived.data
|
|
3264
|
+
].map(normalizeWorkspace);
|
|
3265
|
+
}
|
|
3266
|
+
async getWorkspace(workspaceRef, options) {
|
|
3267
|
+
try {
|
|
3268
|
+
const raw = await this.client.workspaces.get(workspaceRef, options);
|
|
3269
|
+
return normalizeWorkspaceDetail(raw);
|
|
3270
|
+
} catch (err) {
|
|
3271
|
+
const status = err.statusCode;
|
|
3272
|
+
if (status !== 404) throw err;
|
|
3273
|
+
const resolved = await this.resolveWorkspaceRef(workspaceRef, [
|
|
3274
|
+
options?.plane ?? "data",
|
|
3275
|
+
"admin"
|
|
3276
|
+
]);
|
|
3277
|
+
if (resolved === workspaceRef) throw err;
|
|
3278
|
+
const raw = await this.client.workspaces.get(resolved, options);
|
|
3279
|
+
return normalizeWorkspaceDetail(raw);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
async createWorkspace(request) {
|
|
3283
|
+
const body = {
|
|
3284
|
+
name: request.name,
|
|
3285
|
+
scope_name: request.scopeName
|
|
3286
|
+
};
|
|
3287
|
+
if (request.description !== void 0) body.description = request.description;
|
|
3288
|
+
if (request.icon !== void 0) body.icon = request.icon;
|
|
3289
|
+
if (request.defaults !== void 0) body.defaults = request.defaults;
|
|
3290
|
+
if (request.users !== void 0) body.users = request.users;
|
|
3291
|
+
if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
|
|
3292
|
+
if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
|
|
3293
|
+
const created = await this.client.workspaces.create(body);
|
|
3294
|
+
return this.refetchAfterWrite(created.id, created);
|
|
2795
3295
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
}
|
|
2808
|
-
function renderCustomerAppConsumption(data, format = "pretty") {
|
|
2809
|
-
const fmt = (n, scale) => n == null ? "-" : `${n}${scale ?? ""}`;
|
|
2810
|
-
if (format !== "pretty") {
|
|
2811
|
-
const rows = data.detectors.map((d) => ({
|
|
2812
|
-
app_name: data.appName,
|
|
2813
|
-
app_id: data.appId,
|
|
2814
|
-
monitoring_since: data.monitoringSince ?? "",
|
|
2815
|
-
daily_avg: fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale),
|
|
2816
|
-
monthly_total: fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale),
|
|
2817
|
-
sessions_total: data.sessions.total,
|
|
2818
|
-
sessions_violating: data.sessions.violating,
|
|
2819
|
-
detector: d.type,
|
|
2820
|
-
critical: d.critical,
|
|
2821
|
-
high: d.high,
|
|
2822
|
-
medium: d.medium,
|
|
2823
|
-
low: d.low,
|
|
2824
|
-
total: d.total
|
|
2825
|
-
}));
|
|
2826
|
-
console.log(
|
|
2827
|
-
formatOutput(
|
|
2828
|
-
rows,
|
|
2829
|
-
[
|
|
2830
|
-
{ key: "app_name", label: "App" },
|
|
2831
|
-
{ key: "app_id", label: "AppId" },
|
|
2832
|
-
{ key: "monitoring_since", label: "MonitoringSince" },
|
|
2833
|
-
{ key: "daily_avg", label: "DailyAvg" },
|
|
2834
|
-
{ key: "monthly_total", label: "MonthlyTotal" },
|
|
2835
|
-
{ key: "sessions_total", label: "Sessions" },
|
|
2836
|
-
{ key: "sessions_violating", label: "Violating" },
|
|
2837
|
-
{ key: "detector", label: "Detector" },
|
|
2838
|
-
{ key: "critical", label: "C" },
|
|
2839
|
-
{ key: "high", label: "H" },
|
|
2840
|
-
{ key: "medium", label: "M" },
|
|
2841
|
-
{ key: "low", label: "L" },
|
|
2842
|
-
{ key: "total", label: "Total" }
|
|
2843
|
-
],
|
|
2844
|
-
format
|
|
2845
|
-
)
|
|
2846
|
-
);
|
|
2847
|
-
return;
|
|
3296
|
+
async updateWorkspace(workspaceRef, request) {
|
|
3297
|
+
const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
|
|
3298
|
+
const body = {};
|
|
3299
|
+
if (request.name !== void 0) body.name = request.name;
|
|
3300
|
+
if (request.description !== void 0) body.description = request.description;
|
|
3301
|
+
if (request.icon !== void 0) body.icon = request.icon;
|
|
3302
|
+
if (request.defaults !== void 0) body.defaults = request.defaults;
|
|
3303
|
+
if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
|
|
3304
|
+
if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
|
|
3305
|
+
await this.client.workspaces.update(ref, body);
|
|
3306
|
+
return this.getWorkspace(ref, { plane: "admin" });
|
|
2848
3307
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
if (data.source) pairs.push(["Source", data.source]);
|
|
2853
|
-
if (data.cloud) pairs.push(["Cloud", data.cloud]);
|
|
2854
|
-
if (data.profiles.length > 0) pairs.push(["Profiles", data.profiles.join(", ")]);
|
|
2855
|
-
if (pairs.length > 0) ui.keyValue(pairs);
|
|
2856
|
-
ui.section("Token consumption:");
|
|
2857
|
-
ui.keyValue([
|
|
2858
|
-
["Daily avg", fmt(data.tokens.dailyAverage, data.tokens.dailyAverageScale)],
|
|
2859
|
-
["Monthly total", fmt(data.tokens.monthlyTotal, data.tokens.monthlyTotalScale)]
|
|
2860
|
-
]);
|
|
2861
|
-
ui.section("Sessions:");
|
|
2862
|
-
ui.keyValue([
|
|
2863
|
-
["Total", data.sessions.total],
|
|
2864
|
-
["Violating", data.sessions.violating]
|
|
2865
|
-
]);
|
|
2866
|
-
const firing = data.detectors.filter((d) => d.total > 0);
|
|
2867
|
-
ui.section(
|
|
2868
|
-
`Detectors (${data.totalViolating} violating, ${firing.length}/${data.detectors.length} firing):`
|
|
2869
|
-
);
|
|
2870
|
-
if (firing.length === 0) {
|
|
2871
|
-
ui.dim("no detector violations in window");
|
|
2872
|
-
} else {
|
|
2873
|
-
ui.table(
|
|
2874
|
-
[
|
|
2875
|
-
{ key: "detector", label: "Detector" },
|
|
2876
|
-
{ key: "total", label: "Total" },
|
|
2877
|
-
{ key: "severity", label: "Severity" }
|
|
2878
|
-
],
|
|
2879
|
-
firing.map((d) => ({
|
|
2880
|
-
detector: d.type,
|
|
2881
|
-
total: d.total,
|
|
2882
|
-
severity: `c=${d.critical} h=${d.high} m=${d.medium} l=${d.low}`
|
|
2883
|
-
}))
|
|
2884
|
-
);
|
|
3308
|
+
async deleteWorkspace(workspaceRef) {
|
|
3309
|
+
const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
|
|
3310
|
+
await this.client.workspaces.delete(ref);
|
|
2885
3311
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3312
|
+
/**
|
|
3313
|
+
* The API accepts only a UUID or slug as a workspace ref — a display name
|
|
3314
|
+
* gets a misleading 400 AB01 ("No update fields provided") on writes.
|
|
3315
|
+
* Match a user-supplied ref against the workspace list so name | slug |
|
|
3316
|
+
* uuid all work. Unmatched refs pass through so the API's own error stands.
|
|
3317
|
+
*/
|
|
3318
|
+
async resolveWorkspaceRef(ref, planes, requireSlug = false) {
|
|
3319
|
+
for (const plane of planes) {
|
|
3320
|
+
let rows;
|
|
3321
|
+
try {
|
|
3322
|
+
rows = await this.listWorkspaces({ plane });
|
|
3323
|
+
} catch {
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
const byRef = rows.find((w) => w.id === ref || w.slug === ref);
|
|
3327
|
+
if (byRef) return requireSlug ? byRef.slug : ref;
|
|
3328
|
+
const byName = rows.filter((w) => w.name === ref);
|
|
3329
|
+
if (byName.length > 1) {
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`workspace name '${ref}' is ambiguous (${byName.map((w) => w.slug).join(", ")}) \u2014 use a slug or UUID`
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
if (byName.length === 1) return byName[0].slug;
|
|
3335
|
+
}
|
|
3336
|
+
return ref;
|
|
2892
3337
|
}
|
|
2893
|
-
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
3338
|
+
async getTelemetryCost(opts) {
|
|
3339
|
+
const { workspaceSlug: workspaceRef, days = 7, ...rawFilters } = opts;
|
|
3340
|
+
const filters = AIGatewayChartFiltersSchema.parse(rawFilters);
|
|
3341
|
+
if (!Number.isSafeInteger(days) || days <= 0)
|
|
3342
|
+
throw new Error("Expected days to be a positive integer");
|
|
3343
|
+
const workspaceSlug = await this.resolveWorkspaceRef(workspaceRef, ["data", "admin"], true);
|
|
3344
|
+
const raw = await this.client.telemetry.cost({
|
|
3345
|
+
workspaceSlug,
|
|
3346
|
+
days,
|
|
3347
|
+
...filters
|
|
3348
|
+
});
|
|
3349
|
+
return {
|
|
3350
|
+
workspaceSlug,
|
|
3351
|
+
days,
|
|
3352
|
+
totalCents: raw.data.total,
|
|
3353
|
+
totalUsd: raw.data.total / 100,
|
|
3354
|
+
avgCents: raw.data.avg,
|
|
3355
|
+
avgUsd: raw.data.avg / 100,
|
|
3356
|
+
quotaExceeded: raw.data.isQuotaExceeded,
|
|
3357
|
+
records: raw.data.records.map((r) => ({
|
|
3358
|
+
date: r.x,
|
|
3359
|
+
costCents: r.y,
|
|
3360
|
+
costUsd: r.y / 100
|
|
3361
|
+
}))
|
|
3362
|
+
};
|
|
2911
3363
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
` ${name}${status ? ` ${statusColor5(status)}` : ""}${authCode ? ` ${chalk9.dim(authCode)}` : ""}`
|
|
2920
|
-
);
|
|
3364
|
+
/** Re-read after a write, falling back to the (partial) write response if the get fails. */
|
|
3365
|
+
async refetchAfterWrite(workspaceRef, writeResponse) {
|
|
3366
|
+
try {
|
|
3367
|
+
return await this.getWorkspace(workspaceRef, { plane: "admin" });
|
|
3368
|
+
} catch {
|
|
3369
|
+
return normalizeWorkspaceDetail(writeResponse);
|
|
3370
|
+
}
|
|
2921
3371
|
}
|
|
2922
|
-
|
|
2923
|
-
}
|
|
3372
|
+
};
|
|
2924
3373
|
|
|
2925
3374
|
// src/cli/confirm.ts
|
|
2926
3375
|
async function confirmOrAbort(message, force, options = {}) {
|
|
@@ -2958,8 +3407,8 @@ import {
|
|
|
2958
3407
|
GatewayInferenceInputCreateEmbeddingRequestSchema,
|
|
2959
3408
|
GatewayInferenceInputCreateResponseSchema
|
|
2960
3409
|
} from "@cdot65/prisma-airs-sdk";
|
|
2961
|
-
import { dump as
|
|
2962
|
-
function
|
|
3410
|
+
import { dump as dump4 } from "js-yaml";
|
|
3411
|
+
function integer2(raw, flag) {
|
|
2963
3412
|
const value = Number(raw);
|
|
2964
3413
|
if (!Number.isSafeInteger(value) || value < 1)
|
|
2965
3414
|
throw new CliUsageError(`${flag} must be a positive integer`);
|
|
@@ -2992,11 +3441,11 @@ async function inferenceBody(kind, prompt, opts, defaultModel) {
|
|
|
2992
3441
|
body.model = model;
|
|
2993
3442
|
if (opts.stream !== void 0) body.stream = opts.stream;
|
|
2994
3443
|
if (opts.maxTokens !== void 0)
|
|
2995
|
-
body[kind === "responses" ? "max_output_tokens" : "max_completion_tokens"] =
|
|
3444
|
+
body[kind === "responses" ? "max_output_tokens" : "max_completion_tokens"] = integer2(
|
|
2996
3445
|
opts.maxTokens,
|
|
2997
3446
|
"--max-tokens"
|
|
2998
3447
|
);
|
|
2999
|
-
if (opts.dimensions !== void 0) body.dimensions =
|
|
3448
|
+
if (opts.dimensions !== void 0) body.dimensions = integer2(opts.dimensions, "--dimensions");
|
|
3000
3449
|
if (opts.encoding !== void 0) body.encoding_format = opts.encoding;
|
|
3001
3450
|
if (kind === "responses" && body.store === void 0) body.store = false;
|
|
3002
3451
|
return body;
|
|
@@ -3052,14 +3501,14 @@ function registerAiGatewayInference(parent) {
|
|
|
3052
3501
|
const client = new AIGatewayInferenceClient({
|
|
3053
3502
|
endpoint: config.aiGwInferenceEndpoint,
|
|
3054
3503
|
apiKey: config.aiGwInferenceApiKey,
|
|
3055
|
-
timeoutMs:
|
|
3504
|
+
timeoutMs: integer2(opts.timeout ?? "60000", "--timeout"),
|
|
3056
3505
|
numRetries: 0
|
|
3057
3506
|
});
|
|
3058
3507
|
process.once("SIGINT", interrupt);
|
|
3059
3508
|
process.once("SIGTERM", interrupt);
|
|
3060
3509
|
process.stdout.on("error", outputError);
|
|
3061
|
-
const write = (text2) => new Promise((
|
|
3062
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3510
|
+
const write = (text2) => new Promise((resolve7, reject) => {
|
|
3511
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve7());
|
|
3063
3512
|
});
|
|
3064
3513
|
const requestOptions = { signal: controller.signal };
|
|
3065
3514
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -3086,7 +3535,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3086
3535
|
}
|
|
3087
3536
|
if (format === "pretty") await write("\n");
|
|
3088
3537
|
} else {
|
|
3089
|
-
const text2 = format === "yaml" ?
|
|
3538
|
+
const text2 = format === "yaml" ? dump4(result, { noRefs: true }).trimEnd() : format === "pretty" && kind !== "embeddings" ? responseText(result) : JSON.stringify(result, null, 2);
|
|
3090
3539
|
await write(`${text2}
|
|
3091
3540
|
`);
|
|
3092
3541
|
if ("status" in result && (result.status === "failed" || result.status === "incomplete"))
|
|
@@ -3224,7 +3673,7 @@ function headersToRecord(headers) {
|
|
|
3224
3673
|
}
|
|
3225
3674
|
return headers;
|
|
3226
3675
|
}
|
|
3227
|
-
function installDebugLogger(logPath) {
|
|
3676
|
+
function installDebugLogger(logPath, options = {}) {
|
|
3228
3677
|
mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
|
|
3229
3678
|
writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
3230
3679
|
const originalFetch = globalThis.fetch;
|
|
@@ -3239,7 +3688,7 @@ function installDebugLogger(logPath) {
|
|
|
3239
3688
|
if (!isAirsUrl(url) && !inference) {
|
|
3240
3689
|
return originalFetch(input, init2);
|
|
3241
3690
|
}
|
|
3242
|
-
const dashboard = /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url);
|
|
3691
|
+
const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
|
|
3243
3692
|
const method = init2?.method ?? (input instanceof Request ? input.method : "GET");
|
|
3244
3693
|
const reqHeaders = redactHeaders(rawHeaders);
|
|
3245
3694
|
const loggedUrl = redactUrl(url);
|
|
@@ -3317,7 +3766,7 @@ function installDebugLogger(logPath) {
|
|
|
3317
3766
|
// src/cli/commands/aigateway/shared.ts
|
|
3318
3767
|
import { open, readFile as readFile2, unlink } from "fs/promises";
|
|
3319
3768
|
import { AIGatewayClient as AIGatewayClient2 } from "@cdot65/prisma-airs-sdk";
|
|
3320
|
-
import { dump as
|
|
3769
|
+
import { dump as dump5, load } from "js-yaml";
|
|
3321
3770
|
async function defaultClientFactory() {
|
|
3322
3771
|
const config = await loadConfig();
|
|
3323
3772
|
return new AIGatewayClient2(aiGatewayClientOptions(config));
|
|
@@ -3398,7 +3847,7 @@ function renderGatewayList(items, format, label) {
|
|
|
3398
3847
|
return;
|
|
3399
3848
|
}
|
|
3400
3849
|
if (format === "yaml") {
|
|
3401
|
-
console.log(
|
|
3850
|
+
console.log(dump5(items, { noRefs: true, lineWidth: -1 }).trimEnd());
|
|
3402
3851
|
return;
|
|
3403
3852
|
}
|
|
3404
3853
|
if (items.length === 0) {
|
|
@@ -3416,7 +3865,7 @@ function renderGatewayDetail(value, format) {
|
|
|
3416
3865
|
return;
|
|
3417
3866
|
}
|
|
3418
3867
|
if (format === "yaml") {
|
|
3419
|
-
console.log(
|
|
3868
|
+
console.log(dump5(item, { noRefs: true, lineWidth: -1 }).trimEnd());
|
|
3420
3869
|
return;
|
|
3421
3870
|
}
|
|
3422
3871
|
const rows = Object.entries(item).map(([key, raw]) => ({
|
|
@@ -4746,23 +5195,23 @@ function parseNonnegativePage(value) {
|
|
|
4746
5195
|
return Number(value);
|
|
4747
5196
|
}
|
|
4748
5197
|
|
|
4749
|
-
// src/cli/commands/aigateway-
|
|
4750
|
-
import { lstat } from "fs/promises";
|
|
4751
|
-
import { resolve } from "path";
|
|
5198
|
+
// src/cli/commands/aigateway-report.ts
|
|
5199
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
5200
|
+
import { resolve as resolve2 } from "path";
|
|
4752
5201
|
import { AIGatewayClient as AIGatewayClient3 } from "@cdot65/prisma-airs-sdk";
|
|
4753
|
-
import { z } from "zod";
|
|
4754
|
-
import { randomUUID } from "crypto";
|
|
4755
|
-
function
|
|
4756
|
-
const command = aigateway.command("dashboard").description("Generate an AI Gateway environment
|
|
5202
|
+
import { z as z2 } from "zod";
|
|
5203
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5204
|
+
function registerAiGatewayReportCommand(aigateway) {
|
|
5205
|
+
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
5206
|
"--output-file <path>",
|
|
4758
5207
|
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
4759
5208
|
).option("--title <text>", "Report title", "AI Gateway environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (50 transactions/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
4760
5209
|
"after",
|
|
4761
5210
|
examples(
|
|
4762
|
-
"airs aigateway
|
|
4763
|
-
"airs aigateway
|
|
4764
|
-
"airs aigateway
|
|
4765
|
-
"airs aigateway
|
|
5211
|
+
"airs aigateway report --workspace ws-develo-71f8d8 --output-file ./airs-daily.html",
|
|
5212
|
+
"airs aigateway report --workspace ws-develo-71f8d8 --output markdown --output-file ./airs-daily.md",
|
|
5213
|
+
"airs aigateway report --workspace ws-develo-71f8d8",
|
|
5214
|
+
"airs aigateway report --workspace ws-develo-71f8d8 --strict --max-pages 20 --output-file - > daily.html"
|
|
4766
5215
|
)
|
|
4767
5216
|
).action(async (opts) => {
|
|
4768
5217
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
@@ -4778,7 +5227,7 @@ function registerAiGatewayDashboardCommand(aigateway) {
|
|
|
4778
5227
|
usageError("Use --days or --start/--end, not both");
|
|
4779
5228
|
const end = opts.end ? new Date(opts.end) : /* @__PURE__ */ new Date();
|
|
4780
5229
|
const start = opts.start ? new Date(opts.start) : new Date(end.getTime() - Number(opts.days) * 864e5);
|
|
4781
|
-
const iso =
|
|
5230
|
+
const iso = z2.string().datetime({ offset: true });
|
|
4782
5231
|
if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
|
|
4783
5232
|
usageError("Provide a valid increasing ISO-8601 window with timezone");
|
|
4784
5233
|
const maxPages = Number(opts.maxPages);
|
|
@@ -4788,11 +5237,11 @@ function registerAiGatewayDashboardCommand(aigateway) {
|
|
|
4788
5237
|
usageError("--title must contain 1\u2013240 characters");
|
|
4789
5238
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
4790
5239
|
usageError("--output-file cannot be empty");
|
|
4791
|
-
const defaultName = `airs-aigateway-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
4792
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
5240
|
+
const defaultName = `airs-aigateway-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID2().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
5241
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve2(opts.outputFile ?? defaultName);
|
|
4793
5242
|
try {
|
|
4794
5243
|
if (outputPath) {
|
|
4795
|
-
const existing = await
|
|
5244
|
+
const existing = await lstat2(outputPath).catch((error) => {
|
|
4796
5245
|
if (error.code !== "ENOENT") throw error;
|
|
4797
5246
|
return void 0;
|
|
4798
5247
|
});
|
|
@@ -4896,7 +5345,7 @@ function scopeNameLooksUnrelated(name, scopeName) {
|
|
|
4896
5345
|
function registerAiGatewayCommand(program) {
|
|
4897
5346
|
const aigateway = program.command("aigateway").description("Manage and observe Prisma AIRS AI Gateway resources").action(() => aigateway.outputHelp());
|
|
4898
5347
|
registerAiGatewayInventory(aigateway);
|
|
4899
|
-
|
|
5348
|
+
registerAiGatewayReportCommand(aigateway);
|
|
4900
5349
|
registerAiGatewayInference(aigateway);
|
|
4901
5350
|
const workspace = aigateway.command("workspaces").alias("workspace").description("Manage gateway workspaces").action(() => workspace.outputHelp());
|
|
4902
5351
|
const workspaceList = workspace.command("list").description("List workspaces (default: active workspaces you are scoped to)").option("--plane <plane>", "Plane to read from: data (scoped) or admin (whole tenant)").option("--status <status>", "Filter by lifecycle state: active or archived").option("--all", "Merge active + archived admin-plane reads (whole tenant, both states)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
|
|
@@ -5339,7 +5788,7 @@ function registerConfigCommand(program) {
|
|
|
5339
5788
|
}
|
|
5340
5789
|
|
|
5341
5790
|
// src/cli/commands/doctor.ts
|
|
5342
|
-
import { randomUUID as
|
|
5791
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5343
5792
|
import { readFile as readFile4 } from "fs/promises";
|
|
5344
5793
|
import { init, Scanner } from "@cdot65/prisma-airs-sdk";
|
|
5345
5794
|
var DOCTOR_TIMEOUT_MS = 5e3;
|
|
@@ -5437,8 +5886,8 @@ async function withTimeout(promise, ms) {
|
|
|
5437
5886
|
try {
|
|
5438
5887
|
return await Promise.race([
|
|
5439
5888
|
promise,
|
|
5440
|
-
new Promise((
|
|
5441
|
-
timer = setTimeout(() =>
|
|
5889
|
+
new Promise((resolve7) => {
|
|
5890
|
+
timer = setTimeout(() => resolve7(TIMED_OUT), ms);
|
|
5442
5891
|
})
|
|
5443
5892
|
]);
|
|
5444
5893
|
} finally {
|
|
@@ -5593,7 +6042,7 @@ async function defaultScannerProbe() {
|
|
|
5593
6042
|
const config = await loadConfig();
|
|
5594
6043
|
init(runtimeInitOptions(config));
|
|
5595
6044
|
const scanner = new Scanner();
|
|
5596
|
-
return scanner.queryByScanIds([
|
|
6045
|
+
return scanner.queryByScanIds([randomUUID3()]);
|
|
5597
6046
|
}
|
|
5598
6047
|
async function defaultMgmtProbe() {
|
|
5599
6048
|
const config = await loadConfig();
|
|
@@ -5698,6 +6147,21 @@ function registerDoctorCommand(program) {
|
|
|
5698
6147
|
// src/cli/commands/modelsecurity.ts
|
|
5699
6148
|
import { execFileSync, spawn } from "child_process";
|
|
5700
6149
|
import * as fs from "fs";
|
|
6150
|
+
import { z as z3 } from "zod";
|
|
6151
|
+
function supplyChainWindow(opts) {
|
|
6152
|
+
const parsed = z3.object({
|
|
6153
|
+
start: z3.string().datetime({ offset: true }).optional(),
|
|
6154
|
+
end: z3.string().datetime({ offset: true }).optional(),
|
|
6155
|
+
modelVersion: z3.string().uuid().optional()
|
|
6156
|
+
}).refine((v) => !v.start || !v.end || Date.parse(v.start) <= Date.parse(v.end)).safeParse(opts);
|
|
6157
|
+
if (!parsed.success)
|
|
6158
|
+
usageError("Use valid ISO timestamps with start <= end and a valid model-version UUID.");
|
|
6159
|
+
return {
|
|
6160
|
+
startTime: parsed.data.start,
|
|
6161
|
+
endTime: parsed.data.end,
|
|
6162
|
+
modelVersionUuid: parsed.data.modelVersion
|
|
6163
|
+
};
|
|
6164
|
+
}
|
|
5701
6165
|
var VALID_EXTRAS = ["all", "aws", "gcp", "azure", "artifactory", "gitlab"];
|
|
5702
6166
|
function hasBin(bin) {
|
|
5703
6167
|
try {
|
|
@@ -5707,13 +6171,13 @@ function hasBin(bin) {
|
|
|
5707
6171
|
return false;
|
|
5708
6172
|
}
|
|
5709
6173
|
}
|
|
5710
|
-
function
|
|
5711
|
-
return new Promise((
|
|
6174
|
+
function run2(bin, args, label) {
|
|
6175
|
+
return new Promise((resolve7, reject) => {
|
|
5712
6176
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
5713
6177
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
5714
6178
|
child.on("close", (code) => {
|
|
5715
6179
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
5716
|
-
else
|
|
6180
|
+
else resolve7();
|
|
5717
6181
|
});
|
|
5718
6182
|
});
|
|
5719
6183
|
}
|
|
@@ -5847,7 +6311,7 @@ function registerModelSecurityCommand(program) {
|
|
|
5847
6311
|
}
|
|
5848
6312
|
for (const step of steps) {
|
|
5849
6313
|
ui.status(`\u2192 ${step.label}`);
|
|
5850
|
-
await
|
|
6314
|
+
await run2(step.bin, step.args, step.label);
|
|
5851
6315
|
}
|
|
5852
6316
|
ui.success("model-security-client installed successfully.");
|
|
5853
6317
|
if (useUv) {
|
|
@@ -6003,7 +6467,7 @@ function registerModelSecurityCommand(program) {
|
|
|
6003
6467
|
}
|
|
6004
6468
|
});
|
|
6005
6469
|
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(
|
|
6470
|
+
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
6471
|
"after",
|
|
6008
6472
|
examples(
|
|
6009
6473
|
"airs model-security scans list",
|
|
@@ -6017,6 +6481,7 @@ function registerModelSecurityCommand(program) {
|
|
|
6017
6481
|
const service = await createService2();
|
|
6018
6482
|
const listOptions = {
|
|
6019
6483
|
evalOutcome: opts.evalOutcome,
|
|
6484
|
+
...supplyChainWindow(opts),
|
|
6020
6485
|
sourceType: opts.sourceType,
|
|
6021
6486
|
scanOrigin: opts.scanOrigin,
|
|
6022
6487
|
search: opts.search,
|
|
@@ -6111,7 +6576,7 @@ function registerModelSecurityCommand(program) {
|
|
|
6111
6576
|
}
|
|
6112
6577
|
});
|
|
6113
6578
|
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) => {
|
|
6579
|
+
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
6580
|
try {
|
|
6116
6581
|
const fmt = await resolveOutput(modelsList, opts);
|
|
6117
6582
|
if (fmt === "pretty") renderModelSecurityHeader();
|
|
@@ -6119,6 +6584,7 @@ function registerModelSecurityCommand(program) {
|
|
|
6119
6584
|
const listOptions = {
|
|
6120
6585
|
search: opts.search,
|
|
6121
6586
|
searchQuery: opts.searchQuery,
|
|
6587
|
+
...supplyChainWindow(opts),
|
|
6122
6588
|
sortField: opts.sortField,
|
|
6123
6589
|
sortOrder: opts.sortOrder,
|
|
6124
6590
|
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
|
|
@@ -6291,24 +6757,33 @@ async function backupTargets(opts) {
|
|
|
6291
6757
|
return results;
|
|
6292
6758
|
}
|
|
6293
6759
|
|
|
6294
|
-
// src/cli/commands/redteam-
|
|
6295
|
-
import { lstat as
|
|
6296
|
-
import { resolve as
|
|
6760
|
+
// src/cli/commands/redteam-report.ts
|
|
6761
|
+
import { lstat as lstat3 } from "fs/promises";
|
|
6762
|
+
import { resolve as resolve3 } from "path";
|
|
6297
6763
|
import { RedTeamClient } from "@cdot65/prisma-airs-sdk";
|
|
6298
|
-
import { randomUUID as
|
|
6299
|
-
function
|
|
6300
|
-
const command = redteam.command("dashboard").description("Generate
|
|
6764
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6765
|
+
function registerRedTeamReportCommand(redteam, scanReport) {
|
|
6766
|
+
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
6767
|
"--output-file <path>",
|
|
6302
6768
|
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
6303
6769
|
).option("--title <text>", "Report title", "Red Team environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (15 records/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
6304
6770
|
"after",
|
|
6305
6771
|
examples(
|
|
6306
|
-
"airs redteam
|
|
6307
|
-
"airs redteam
|
|
6308
|
-
"airs redteam
|
|
6309
|
-
"airs redteam
|
|
6772
|
+
"airs redteam report --output-file ./airs-daily.html",
|
|
6773
|
+
"airs redteam report --output markdown --output-file ./airs-daily.md",
|
|
6774
|
+
"airs redteam report",
|
|
6775
|
+
"airs redteam report --strict --max-pages 20 --output-file - > daily.html"
|
|
6310
6776
|
)
|
|
6311
|
-
).action(async (opts) => {
|
|
6777
|
+
).action(async (jobId, opts) => {
|
|
6778
|
+
const explicit = (name) => command.getOptionValueSource(name) === "cli";
|
|
6779
|
+
if (jobId !== void 0) {
|
|
6780
|
+
if (["output", "outputFile", "title", "maxPages", "strict"].some(explicit) || ["html", "markdown"].includes(command.parent?.parent?.opts().output))
|
|
6781
|
+
usageError("Environment report options cannot be combined with a scan job ID");
|
|
6782
|
+
await scanReport(jobId, opts);
|
|
6783
|
+
return;
|
|
6784
|
+
}
|
|
6785
|
+
if (["attacks", "severity", "limit"].some(explicit))
|
|
6786
|
+
usageError("--attacks, --severity and --limit require a scan job ID");
|
|
6312
6787
|
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6313
6788
|
if (format !== "html" && format !== "markdown")
|
|
6314
6789
|
usageError("Report output must be html or markdown");
|
|
@@ -6319,11 +6794,11 @@ function registerRedTeamDashboardCommand(redteam) {
|
|
|
6319
6794
|
usageError("--title must contain 1\u2013240 characters");
|
|
6320
6795
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
6321
6796
|
usageError("--output-file cannot be empty");
|
|
6322
|
-
const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
6323
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
6797
|
+
const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID4().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
6798
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve3(opts.outputFile ?? defaultName);
|
|
6324
6799
|
try {
|
|
6325
6800
|
if (outputPath) {
|
|
6326
|
-
const existing = await
|
|
6801
|
+
const existing = await lstat3(outputPath).catch((error) => {
|
|
6327
6802
|
if (error.code !== "ENOENT") throw error;
|
|
6328
6803
|
return void 0;
|
|
6329
6804
|
});
|
|
@@ -6603,7 +7078,6 @@ function parseAdapterVariables(input) {
|
|
|
6603
7078
|
}
|
|
6604
7079
|
function registerRedteamCommand(program) {
|
|
6605
7080
|
const redteam = program.command("redteam").description("AI Red Team scan operations");
|
|
6606
|
-
registerRedTeamDashboardCommand(redteam);
|
|
6607
7081
|
redteam.command("abort <jobId>").description("Abort a running scan").action(async (jobId) => {
|
|
6608
7082
|
try {
|
|
6609
7083
|
renderRedteamHeader();
|
|
@@ -6986,7 +7460,7 @@ function registerRedteamCommand(program) {
|
|
|
6986
7460
|
fail(err);
|
|
6987
7461
|
}
|
|
6988
7462
|
});
|
|
6989
|
-
redteam
|
|
7463
|
+
registerRedTeamReportCommand(redteam, async (jobId, opts) => {
|
|
6990
7464
|
try {
|
|
6991
7465
|
renderRedteamHeader();
|
|
6992
7466
|
const service = await createService3();
|
|
@@ -7597,7 +8071,7 @@ function registerRedteamCommand(program) {
|
|
|
7597
8071
|
}
|
|
7598
8072
|
|
|
7599
8073
|
// src/cli/commands/runtime.ts
|
|
7600
|
-
import { randomUUID as
|
|
8074
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7601
8075
|
import * as fs5 from "fs";
|
|
7602
8076
|
import { readFile as readFile10 } from "fs/promises";
|
|
7603
8077
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
@@ -7845,7 +8319,7 @@ function mergeProfilePolicy(existing, overrides) {
|
|
|
7845
8319
|
}
|
|
7846
8320
|
|
|
7847
8321
|
// src/cli/bulk-scan-lock.ts
|
|
7848
|
-
import { randomUUID as
|
|
8322
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7849
8323
|
import * as fs3 from "fs/promises";
|
|
7850
8324
|
function errorCode(error) {
|
|
7851
8325
|
return error?.code;
|
|
@@ -7876,7 +8350,7 @@ function processIsAlive(pid) {
|
|
|
7876
8350
|
}
|
|
7877
8351
|
}
|
|
7878
8352
|
async function installLock(lockPath, record) {
|
|
7879
|
-
const candidate = `${lockPath}.candidate-${process.pid}-${
|
|
8353
|
+
const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID5()}`;
|
|
7880
8354
|
try {
|
|
7881
8355
|
await fs3.writeFile(candidate, JSON.stringify(record), {
|
|
7882
8356
|
encoding: "utf-8",
|
|
@@ -7894,7 +8368,7 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7894
8368
|
version: 1,
|
|
7895
8369
|
pid: process.pid,
|
|
7896
8370
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7897
|
-
token:
|
|
8371
|
+
token: randomUUID5()
|
|
7898
8372
|
};
|
|
7899
8373
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7900
8374
|
try {
|
|
@@ -7930,32 +8404,32 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7930
8404
|
}
|
|
7931
8405
|
|
|
7932
8406
|
// src/cli/bulk-scan-state.ts
|
|
7933
|
-
import { randomUUID as
|
|
8407
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7934
8408
|
import * as fs4 from "fs/promises";
|
|
7935
8409
|
import * as path2 from "path";
|
|
7936
|
-
import { z as
|
|
7937
|
-
var BulkScanResultSchema =
|
|
7938
|
-
index:
|
|
7939
|
-
reqId:
|
|
7940
|
-
prompt:
|
|
7941
|
-
response:
|
|
7942
|
-
scanId:
|
|
7943
|
-
reportId:
|
|
7944
|
-
action:
|
|
7945
|
-
category:
|
|
7946
|
-
triggered:
|
|
7947
|
-
detections:
|
|
7948
|
-
error:
|
|
8410
|
+
import { z as z4 } from "zod";
|
|
8411
|
+
var BulkScanResultSchema = z4.object({
|
|
8412
|
+
index: z4.number().int().nonnegative(),
|
|
8413
|
+
reqId: z4.number().int().nonnegative(),
|
|
8414
|
+
prompt: z4.string(),
|
|
8415
|
+
response: z4.string().optional(),
|
|
8416
|
+
scanId: z4.string(),
|
|
8417
|
+
reportId: z4.string(),
|
|
8418
|
+
action: z4.enum(["allow", "block", "failed"]),
|
|
8419
|
+
category: z4.string(),
|
|
8420
|
+
triggered: z4.boolean(),
|
|
8421
|
+
detections: z4.record(z4.boolean()),
|
|
8422
|
+
error: z4.string().optional()
|
|
7949
8423
|
});
|
|
7950
|
-
var BulkScanItemSchema =
|
|
7951
|
-
index:
|
|
7952
|
-
reqId:
|
|
7953
|
-
prompt:
|
|
7954
|
-
status:
|
|
7955
|
-
scanId:
|
|
7956
|
-
receiptReportId:
|
|
8424
|
+
var BulkScanItemSchema = z4.object({
|
|
8425
|
+
index: z4.number().int().nonnegative(),
|
|
8426
|
+
reqId: z4.number().int().nonnegative(),
|
|
8427
|
+
prompt: z4.string(),
|
|
8428
|
+
status: z4.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
|
|
8429
|
+
scanId: z4.string().min(1).optional(),
|
|
8430
|
+
receiptReportId: z4.string().optional(),
|
|
7957
8431
|
result: BulkScanResultSchema.optional(),
|
|
7958
|
-
error:
|
|
8432
|
+
error: z4.string().optional()
|
|
7959
8433
|
}).superRefine((item, ctx) => {
|
|
7960
8434
|
if (item.reqId !== item.index) {
|
|
7961
8435
|
ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
|
|
@@ -7993,15 +8467,15 @@ var BulkScanItemSchema = z2.object({
|
|
|
7993
8467
|
}
|
|
7994
8468
|
}
|
|
7995
8469
|
});
|
|
7996
|
-
var BulkScanStateSchema =
|
|
7997
|
-
version:
|
|
7998
|
-
profile:
|
|
7999
|
-
sessionId:
|
|
8000
|
-
outputFile:
|
|
8001
|
-
batchSize:
|
|
8002
|
-
createdAt:
|
|
8003
|
-
updatedAt:
|
|
8004
|
-
items:
|
|
8470
|
+
var BulkScanStateSchema = z4.object({
|
|
8471
|
+
version: z4.literal(2),
|
|
8472
|
+
profile: z4.string().min(1),
|
|
8473
|
+
sessionId: z4.string().optional(),
|
|
8474
|
+
outputFile: z4.string().min(1),
|
|
8475
|
+
batchSize: z4.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
8476
|
+
createdAt: z4.string().datetime(),
|
|
8477
|
+
updatedAt: z4.string().datetime(),
|
|
8478
|
+
items: z4.array(BulkScanItemSchema).min(1)
|
|
8005
8479
|
}).superRefine((state, ctx) => {
|
|
8006
8480
|
const indices = /* @__PURE__ */ new Set();
|
|
8007
8481
|
for (const [position, item] of state.items.entries()) {
|
|
@@ -8027,8 +8501,8 @@ async function saveBulkScanState(state, dir, filePath) {
|
|
|
8027
8501
|
}
|
|
8028
8502
|
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
8029
8503
|
if (!filePath) await fs4.chmod(dir, 448);
|
|
8030
|
-
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${
|
|
8031
|
-
const temporary = `${target}.tmp-${process.pid}-${
|
|
8504
|
+
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID6()}.bulk-scan.json`);
|
|
8505
|
+
const temporary = `${target}.tmp-${process.pid}-${randomUUID6()}`;
|
|
8032
8506
|
try {
|
|
8033
8507
|
await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
|
|
8034
8508
|
encoding: "utf-8",
|
|
@@ -9153,11 +9627,11 @@ function registerCleanupCommand(parent) {
|
|
|
9153
9627
|
}
|
|
9154
9628
|
|
|
9155
9629
|
// src/cli/commands/runtime-dashboard.ts
|
|
9156
|
-
import { lstat as
|
|
9157
|
-
import { resolve as
|
|
9158
|
-
import { dump as
|
|
9630
|
+
import { lstat as lstat4 } from "fs/promises";
|
|
9631
|
+
import { resolve as resolve5 } from "path";
|
|
9632
|
+
import { dump as dump6 } from "js-yaml";
|
|
9159
9633
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
9160
|
-
function
|
|
9634
|
+
function integer3(value, name, minimum = 0) {
|
|
9161
9635
|
const n = Number(value);
|
|
9162
9636
|
if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(n) || n < minimum)
|
|
9163
9637
|
throw new CliUsageError(`${name} must be a safe integer >= ${minimum}`);
|
|
@@ -9169,7 +9643,7 @@ function text(value, name) {
|
|
|
9169
9643
|
}
|
|
9170
9644
|
function window(opts) {
|
|
9171
9645
|
return {
|
|
9172
|
-
timeInterval:
|
|
9646
|
+
timeInterval: integer3(opts.interval, "--interval", 1),
|
|
9173
9647
|
timeUnit: text(opts.unit, "--unit")
|
|
9174
9648
|
};
|
|
9175
9649
|
}
|
|
@@ -9184,7 +9658,7 @@ function identity(opts) {
|
|
|
9184
9658
|
function scan(opts) {
|
|
9185
9659
|
return {
|
|
9186
9660
|
scanId: text(opts.scanId, "--scan-id"),
|
|
9187
|
-
scanSubReqId:
|
|
9661
|
+
scanSubReqId: integer3(opts.scanSubReqId, "--scan-sub-req-id")
|
|
9188
9662
|
};
|
|
9189
9663
|
}
|
|
9190
9664
|
function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_UNITS) {
|
|
@@ -9201,7 +9675,7 @@ function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_
|
|
|
9201
9675
|
unit
|
|
9202
9676
|
);
|
|
9203
9677
|
}
|
|
9204
|
-
function
|
|
9678
|
+
function pageOptions2(command) {
|
|
9205
9679
|
return command.option("--limit <n>", "Items per page", "25").option("--offset <n>", "Item offset", "0");
|
|
9206
9680
|
}
|
|
9207
9681
|
function sessionOptions(command) {
|
|
@@ -9230,8 +9704,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9230
9704
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9231
9705
|
);
|
|
9232
9706
|
if (opts.outputFile) {
|
|
9233
|
-
const path3 =
|
|
9234
|
-
const existing = await
|
|
9707
|
+
const path3 = resolve5(text(opts.outputFile, "--output-file"));
|
|
9708
|
+
const existing = await lstat4(path3).catch((error) => {
|
|
9235
9709
|
if (error.code !== "ENOENT") throw error;
|
|
9236
9710
|
return void 0;
|
|
9237
9711
|
});
|
|
@@ -9244,9 +9718,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9244
9718
|
numRetries: 0
|
|
9245
9719
|
});
|
|
9246
9720
|
const result = await read(client.dashboard);
|
|
9247
|
-
const output = format === "yaml" ?
|
|
9721
|
+
const output = format === "yaml" ? dump6(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9248
9722
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9249
|
-
await writeReportFile(
|
|
9723
|
+
await writeReportFile(resolve5(String(opts.outputFile)), `${output}
|
|
9250
9724
|
`);
|
|
9251
9725
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9252
9726
|
} else console.log(output);
|
|
@@ -9261,7 +9735,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9261
9735
|
});
|
|
9262
9736
|
}
|
|
9263
9737
|
register6(
|
|
9264
|
-
|
|
9738
|
+
pageOptions2(
|
|
9265
9739
|
timeOptions(
|
|
9266
9740
|
dashboard.command("applications").description("One page of application activity"),
|
|
9267
9741
|
"1",
|
|
@@ -9270,19 +9744,19 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9270
9744
|
)
|
|
9271
9745
|
),
|
|
9272
9746
|
(opts) => {
|
|
9273
|
-
const interval =
|
|
9747
|
+
const interval = integer3(opts.interval, "--interval", 1);
|
|
9274
9748
|
const unit = text(opts.unit, "--unit");
|
|
9275
9749
|
if (![1, 7, 30, 60].includes(interval) || !["day", "days", "hour"].includes(unit))
|
|
9276
9750
|
throw new CliUsageError(
|
|
9277
9751
|
"Application overview supports interval 1/7/30/60 and unit day/days/hour"
|
|
9278
9752
|
);
|
|
9279
|
-
const
|
|
9753
|
+
const query2 = {
|
|
9280
9754
|
timeInterval: interval,
|
|
9281
9755
|
timeUnit: unit,
|
|
9282
|
-
limit:
|
|
9283
|
-
offset:
|
|
9756
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9757
|
+
offset: integer3(opts.offset, "--offset")
|
|
9284
9758
|
};
|
|
9285
|
-
return (client) => client.applicationsOverview(
|
|
9759
|
+
return (client) => client.applicationsOverview(query2);
|
|
9286
9760
|
}
|
|
9287
9761
|
);
|
|
9288
9762
|
for (const [name, method] of [
|
|
@@ -9297,16 +9771,16 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9297
9771
|
["days"]
|
|
9298
9772
|
),
|
|
9299
9773
|
(opts) => {
|
|
9300
|
-
const interval =
|
|
9774
|
+
const interval = integer3(opts.interval, "--interval", 1);
|
|
9301
9775
|
if (![7, 30, 60].includes(interval) || opts.unit !== "days")
|
|
9302
9776
|
throw new CliUsageError("Application detail requires 7, 30, or 60 days");
|
|
9303
|
-
const
|
|
9777
|
+
const query2 = {
|
|
9304
9778
|
appId: text(opts.appId, "--app-id"),
|
|
9305
9779
|
appName: text(opts.appName, "--app-name"),
|
|
9306
9780
|
timeInterval: interval,
|
|
9307
9781
|
timeUnit: "days"
|
|
9308
9782
|
};
|
|
9309
|
-
return (client) => client[method](
|
|
9783
|
+
return (client) => client[method](query2);
|
|
9310
9784
|
}
|
|
9311
9785
|
);
|
|
9312
9786
|
}
|
|
@@ -9318,35 +9792,35 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9318
9792
|
register6(
|
|
9319
9793
|
timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
|
|
9320
9794
|
(opts) => {
|
|
9321
|
-
const
|
|
9322
|
-
return (client) => client[method](
|
|
9795
|
+
const query2 = window(opts);
|
|
9796
|
+
return (client) => client[method](query2);
|
|
9323
9797
|
}
|
|
9324
9798
|
);
|
|
9325
9799
|
}
|
|
9326
9800
|
register6(timeOptions(sessions.command("chart")), (opts) => {
|
|
9327
|
-
const
|
|
9328
|
-
return (client) => client.sessionsChart(
|
|
9801
|
+
const query2 = window(opts);
|
|
9802
|
+
return (client) => client.sessionsChart(query2);
|
|
9329
9803
|
});
|
|
9330
9804
|
register6(
|
|
9331
|
-
|
|
9805
|
+
pageOptions2(
|
|
9332
9806
|
timeOptions(
|
|
9333
9807
|
sessions.command("list").description("Session entries; --all walks pages with completeness checks")
|
|
9334
9808
|
)
|
|
9335
9809
|
).option("--all", "Walk all pages (up to 100 pages)").option("--max <n>", "Item cap with --all; 0 removes item cap", "10000"),
|
|
9336
9810
|
(opts) => {
|
|
9337
|
-
const
|
|
9811
|
+
const query2 = {
|
|
9338
9812
|
...window(opts),
|
|
9339
|
-
limit:
|
|
9340
|
-
offset:
|
|
9813
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9814
|
+
offset: integer3(opts.offset, "--offset")
|
|
9341
9815
|
};
|
|
9342
|
-
const maximum =
|
|
9816
|
+
const maximum = integer3(opts.max, "--max");
|
|
9343
9817
|
return async (client) => {
|
|
9344
9818
|
const items = [];
|
|
9345
9819
|
const seen = /* @__PURE__ */ new Set();
|
|
9346
9820
|
let expectedTotal;
|
|
9347
|
-
let offset =
|
|
9821
|
+
let offset = query2.offset;
|
|
9348
9822
|
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
9349
|
-
const page = await client.sessionsOverview({ ...
|
|
9823
|
+
const page = await client.sessionsOverview({ ...query2, offset });
|
|
9350
9824
|
if (!opts.all) {
|
|
9351
9825
|
ui.status(
|
|
9352
9826
|
`Returned ${page.items.length} of ${page.pagination.total_items ?? "unknown"} sessions; this is one page.`
|
|
@@ -9354,7 +9828,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9354
9828
|
return page.items;
|
|
9355
9829
|
}
|
|
9356
9830
|
const total = page.pagination.total_items;
|
|
9357
|
-
if (!Number.isSafeInteger(total) || total < 0 || page.pagination.skip !== offset || page.pagination.limit !==
|
|
9831
|
+
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)
|
|
9358
9832
|
throw new Error("Unstable pagination");
|
|
9359
9833
|
expectedTotal = total;
|
|
9360
9834
|
for (const item of page.items) {
|
|
@@ -9377,7 +9851,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9377
9851
|
}
|
|
9378
9852
|
if (offset === total) {
|
|
9379
9853
|
ui.status(
|
|
9380
|
-
`Retrieved ${items.length} sessions from offset ${
|
|
9854
|
+
`Retrieved ${items.length} sessions from offset ${query2.offset}; pagination complete.`
|
|
9381
9855
|
);
|
|
9382
9856
|
return items;
|
|
9383
9857
|
}
|
|
@@ -9393,35 +9867,35 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9393
9867
|
};
|
|
9394
9868
|
}
|
|
9395
9869
|
);
|
|
9396
|
-
register6(
|
|
9397
|
-
const
|
|
9870
|
+
register6(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
|
|
9871
|
+
const query2 = {
|
|
9398
9872
|
...identity(opts),
|
|
9399
|
-
limit:
|
|
9400
|
-
offset:
|
|
9873
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9874
|
+
offset: integer3(opts.offset, "--offset")
|
|
9401
9875
|
};
|
|
9402
|
-
return (client) => client.session(
|
|
9876
|
+
return (client) => client.session(query2);
|
|
9403
9877
|
});
|
|
9404
9878
|
register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
|
|
9405
|
-
const
|
|
9406
|
-
return (client) => client.sessionTransaction(
|
|
9879
|
+
const query2 = { ...identity(opts), ...scan(opts) };
|
|
9880
|
+
return (client) => client.sessionTransaction(query2);
|
|
9407
9881
|
});
|
|
9408
9882
|
register6(
|
|
9409
9883
|
scanOptions(
|
|
9410
9884
|
sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
|
|
9411
9885
|
).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
|
|
9412
9886
|
(opts) => {
|
|
9413
|
-
const
|
|
9414
|
-
return (client) => client.scanContent(
|
|
9887
|
+
const query2 = scan(opts);
|
|
9888
|
+
return (client) => client.scanContent(query2);
|
|
9415
9889
|
}
|
|
9416
9890
|
);
|
|
9417
9891
|
}
|
|
9418
9892
|
|
|
9419
9893
|
// src/cli/commands/runtime-report.ts
|
|
9420
|
-
import { lstat as
|
|
9421
|
-
import { resolve as
|
|
9422
|
-
import { randomUUID as
|
|
9894
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
9895
|
+
import { resolve as resolve6 } from "path";
|
|
9896
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9423
9897
|
function registerRuntimeReportCommand(runtime) {
|
|
9424
|
-
const command = runtime.command("report").description("Generate a daily AI Runtime Security environment
|
|
9898
|
+
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(
|
|
9425
9899
|
"--output-file <path>",
|
|
9426
9900
|
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
9427
9901
|
).option("--title <text>", "Report title", "Daily environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (25 sessions/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
@@ -9443,11 +9917,11 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9443
9917
|
usageError("--title must contain 1\u2013240 characters");
|
|
9444
9918
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9445
9919
|
usageError("--output-file cannot be empty");
|
|
9446
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9447
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
9920
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID7().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
9921
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve6(opts.outputFile ?? defaultName);
|
|
9448
9922
|
try {
|
|
9449
9923
|
if (outputPath) {
|
|
9450
|
-
const existing = await
|
|
9924
|
+
const existing = await lstat5(outputPath).catch((error) => {
|
|
9451
9925
|
if (error.code !== "ENOENT") throw error;
|
|
9452
9926
|
return void 0;
|
|
9453
9927
|
});
|
|
@@ -9934,7 +10408,7 @@ function completedBulkResults(state) {
|
|
|
9934
10408
|
}
|
|
9935
10409
|
async function writeBulkResults(outputPath, results) {
|
|
9936
10410
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
9937
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
10411
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID8()}`;
|
|
9938
10412
|
try {
|
|
9939
10413
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
9940
10414
|
encoding: "utf-8",
|
|
@@ -10737,7 +11211,8 @@ var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
|
10737
11211
|
"version",
|
|
10738
11212
|
"versions",
|
|
10739
11213
|
"violation",
|
|
10740
|
-
"violations"
|
|
11214
|
+
"violations",
|
|
11215
|
+
"vulnerabilities"
|
|
10741
11216
|
]);
|
|
10742
11217
|
function applyListDeleteAliases(cmd) {
|
|
10743
11218
|
for (const sub of cmd.commands) {
|
|
@@ -10779,12 +11254,14 @@ function buildProgram() {
|
|
|
10779
11254
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
10780
11255
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
10781
11256
|
setQuiet(Boolean(root.quiet));
|
|
10782
|
-
const isEnvironmentReport = actionCommand.name() === "report" &&
|
|
11257
|
+
const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway", "agentguard"].includes(
|
|
11258
|
+
actionCommand.parent?.name() ?? ""
|
|
11259
|
+
) && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
|
|
10783
11260
|
if (isEnvironmentReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
10784
11261
|
usageError(
|
|
10785
11262
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
10786
11263
|
);
|
|
10787
|
-
if (!isEnvironmentReport && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
11264
|
+
if (!isEnvironmentReport && !(actionCommand.name() === "report" && actionCommand.parent?.name() === "redteam") && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
10788
11265
|
try {
|
|
10789
11266
|
const format = await resolveOutput(actionCommand, actionCommand.opts());
|
|
10790
11267
|
actionCommand.setOptionValueWithSource("output", format, "implied");
|
|
@@ -10795,10 +11272,16 @@ function buildProgram() {
|
|
|
10795
11272
|
if (root.debug) {
|
|
10796
11273
|
const logPath = join4(
|
|
10797
11274
|
process.cwd(),
|
|
10798
|
-
`debug-api-${Date.now()}-${
|
|
11275
|
+
`debug-api-${Date.now()}-${randomUUID9().slice(0, 8)}.jsonl`
|
|
10799
11276
|
);
|
|
10800
11277
|
try {
|
|
10801
|
-
|
|
11278
|
+
let ancestor = actionCommand;
|
|
11279
|
+
let agentGuard = false;
|
|
11280
|
+
while (ancestor) {
|
|
11281
|
+
if (ancestor.name() === "agentguard") agentGuard = true;
|
|
11282
|
+
ancestor = ancestor.parent;
|
|
11283
|
+
}
|
|
11284
|
+
installDebugLogger(logPath, { omitBodies: agentGuard });
|
|
10802
11285
|
} catch {
|
|
10803
11286
|
ui.error(
|
|
10804
11287
|
"Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
|
|
@@ -10811,6 +11294,7 @@ function buildProgram() {
|
|
|
10811
11294
|
registerRuntimeCommand(program);
|
|
10812
11295
|
registerRedteamCommand(program);
|
|
10813
11296
|
registerModelSecurityCommand(program);
|
|
11297
|
+
registerAgentGuardCommand(program);
|
|
10814
11298
|
registerAiGatewayCommand(program);
|
|
10815
11299
|
registerConfigCommand(program);
|
|
10816
11300
|
registerDoctorCommand(program);
|