@cdot65/prisma-airs-cli 5.3.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/dist/{chunk-7Z76VCB3.js → chunk-36Z6IPH6.js} +18 -2
- package/dist/cli/index.js +904 -428
- package/dist/index.d.ts +14 -0
- 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]) => ({
|
|
@@ -4747,11 +5196,11 @@ function parseNonnegativePage(value) {
|
|
|
4747
5196
|
}
|
|
4748
5197
|
|
|
4749
5198
|
// src/cli/commands/aigateway-report.ts
|
|
4750
|
-
import { lstat } from "fs/promises";
|
|
4751
|
-
import { resolve } from "path";
|
|
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";
|
|
5202
|
+
import { z as z2 } from "zod";
|
|
5203
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4755
5204
|
function registerAiGatewayReportCommand(aigateway) {
|
|
4756
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>",
|
|
@@ -4778,7 +5227,7 @@ function registerAiGatewayReportCommand(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 registerAiGatewayReportCommand(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
|
});
|
|
@@ -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,
|
|
@@ -6292,10 +6758,10 @@ async function backupTargets(opts) {
|
|
|
6292
6758
|
}
|
|
6293
6759
|
|
|
6294
6760
|
// src/cli/commands/redteam-report.ts
|
|
6295
|
-
import { lstat as
|
|
6296
|
-
import { resolve as
|
|
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
|
|
6764
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6299
6765
|
function registerRedTeamReportCommand(redteam, scanReport) {
|
|
6300
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>",
|
|
@@ -6328,11 +6794,11 @@ function registerRedTeamReportCommand(redteam, scanReport) {
|
|
|
6328
6794
|
usageError("--title must contain 1\u2013240 characters");
|
|
6329
6795
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
6330
6796
|
usageError("--output-file cannot be empty");
|
|
6331
|
-
const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
6332
|
-
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);
|
|
6333
6799
|
try {
|
|
6334
6800
|
if (outputPath) {
|
|
6335
|
-
const existing = await
|
|
6801
|
+
const existing = await lstat3(outputPath).catch((error) => {
|
|
6336
6802
|
if (error.code !== "ENOENT") throw error;
|
|
6337
6803
|
return void 0;
|
|
6338
6804
|
});
|
|
@@ -7605,7 +8071,7 @@ function registerRedteamCommand(program) {
|
|
|
7605
8071
|
}
|
|
7606
8072
|
|
|
7607
8073
|
// src/cli/commands/runtime.ts
|
|
7608
|
-
import { randomUUID as
|
|
8074
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7609
8075
|
import * as fs5 from "fs";
|
|
7610
8076
|
import { readFile as readFile10 } from "fs/promises";
|
|
7611
8077
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
@@ -7853,7 +8319,7 @@ function mergeProfilePolicy(existing, overrides) {
|
|
|
7853
8319
|
}
|
|
7854
8320
|
|
|
7855
8321
|
// src/cli/bulk-scan-lock.ts
|
|
7856
|
-
import { randomUUID as
|
|
8322
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7857
8323
|
import * as fs3 from "fs/promises";
|
|
7858
8324
|
function errorCode(error) {
|
|
7859
8325
|
return error?.code;
|
|
@@ -7884,7 +8350,7 @@ function processIsAlive(pid) {
|
|
|
7884
8350
|
}
|
|
7885
8351
|
}
|
|
7886
8352
|
async function installLock(lockPath, record) {
|
|
7887
|
-
const candidate = `${lockPath}.candidate-${process.pid}-${
|
|
8353
|
+
const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID5()}`;
|
|
7888
8354
|
try {
|
|
7889
8355
|
await fs3.writeFile(candidate, JSON.stringify(record), {
|
|
7890
8356
|
encoding: "utf-8",
|
|
@@ -7902,7 +8368,7 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7902
8368
|
version: 1,
|
|
7903
8369
|
pid: process.pid,
|
|
7904
8370
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7905
|
-
token:
|
|
8371
|
+
token: randomUUID5()
|
|
7906
8372
|
};
|
|
7907
8373
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7908
8374
|
try {
|
|
@@ -7938,32 +8404,32 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7938
8404
|
}
|
|
7939
8405
|
|
|
7940
8406
|
// src/cli/bulk-scan-state.ts
|
|
7941
|
-
import { randomUUID as
|
|
8407
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7942
8408
|
import * as fs4 from "fs/promises";
|
|
7943
8409
|
import * as path2 from "path";
|
|
7944
|
-
import { z as
|
|
7945
|
-
var BulkScanResultSchema =
|
|
7946
|
-
index:
|
|
7947
|
-
reqId:
|
|
7948
|
-
prompt:
|
|
7949
|
-
response:
|
|
7950
|
-
scanId:
|
|
7951
|
-
reportId:
|
|
7952
|
-
action:
|
|
7953
|
-
category:
|
|
7954
|
-
triggered:
|
|
7955
|
-
detections:
|
|
7956
|
-
error:
|
|
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()
|
|
7957
8423
|
});
|
|
7958
|
-
var BulkScanItemSchema =
|
|
7959
|
-
index:
|
|
7960
|
-
reqId:
|
|
7961
|
-
prompt:
|
|
7962
|
-
status:
|
|
7963
|
-
scanId:
|
|
7964
|
-
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(),
|
|
7965
8431
|
result: BulkScanResultSchema.optional(),
|
|
7966
|
-
error:
|
|
8432
|
+
error: z4.string().optional()
|
|
7967
8433
|
}).superRefine((item, ctx) => {
|
|
7968
8434
|
if (item.reqId !== item.index) {
|
|
7969
8435
|
ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
|
|
@@ -8001,15 +8467,15 @@ var BulkScanItemSchema = z2.object({
|
|
|
8001
8467
|
}
|
|
8002
8468
|
}
|
|
8003
8469
|
});
|
|
8004
|
-
var BulkScanStateSchema =
|
|
8005
|
-
version:
|
|
8006
|
-
profile:
|
|
8007
|
-
sessionId:
|
|
8008
|
-
outputFile:
|
|
8009
|
-
batchSize:
|
|
8010
|
-
createdAt:
|
|
8011
|
-
updatedAt:
|
|
8012
|
-
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)
|
|
8013
8479
|
}).superRefine((state, ctx) => {
|
|
8014
8480
|
const indices = /* @__PURE__ */ new Set();
|
|
8015
8481
|
for (const [position, item] of state.items.entries()) {
|
|
@@ -8035,8 +8501,8 @@ async function saveBulkScanState(state, dir, filePath) {
|
|
|
8035
8501
|
}
|
|
8036
8502
|
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
8037
8503
|
if (!filePath) await fs4.chmod(dir, 448);
|
|
8038
|
-
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${
|
|
8039
|
-
const temporary = `${target}.tmp-${process.pid}-${
|
|
8504
|
+
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID6()}.bulk-scan.json`);
|
|
8505
|
+
const temporary = `${target}.tmp-${process.pid}-${randomUUID6()}`;
|
|
8040
8506
|
try {
|
|
8041
8507
|
await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
|
|
8042
8508
|
encoding: "utf-8",
|
|
@@ -9161,11 +9627,11 @@ function registerCleanupCommand(parent) {
|
|
|
9161
9627
|
}
|
|
9162
9628
|
|
|
9163
9629
|
// src/cli/commands/runtime-dashboard.ts
|
|
9164
|
-
import { lstat as
|
|
9165
|
-
import { resolve as
|
|
9166
|
-
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";
|
|
9167
9633
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
9168
|
-
function
|
|
9634
|
+
function integer3(value, name, minimum = 0) {
|
|
9169
9635
|
const n = Number(value);
|
|
9170
9636
|
if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(n) || n < minimum)
|
|
9171
9637
|
throw new CliUsageError(`${name} must be a safe integer >= ${minimum}`);
|
|
@@ -9177,7 +9643,7 @@ function text(value, name) {
|
|
|
9177
9643
|
}
|
|
9178
9644
|
function window(opts) {
|
|
9179
9645
|
return {
|
|
9180
|
-
timeInterval:
|
|
9646
|
+
timeInterval: integer3(opts.interval, "--interval", 1),
|
|
9181
9647
|
timeUnit: text(opts.unit, "--unit")
|
|
9182
9648
|
};
|
|
9183
9649
|
}
|
|
@@ -9192,7 +9658,7 @@ function identity(opts) {
|
|
|
9192
9658
|
function scan(opts) {
|
|
9193
9659
|
return {
|
|
9194
9660
|
scanId: text(opts.scanId, "--scan-id"),
|
|
9195
|
-
scanSubReqId:
|
|
9661
|
+
scanSubReqId: integer3(opts.scanSubReqId, "--scan-sub-req-id")
|
|
9196
9662
|
};
|
|
9197
9663
|
}
|
|
9198
9664
|
function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_UNITS) {
|
|
@@ -9209,7 +9675,7 @@ function timeOptions(command, interval = "1", unit = "day", allowedUnits = TIME_
|
|
|
9209
9675
|
unit
|
|
9210
9676
|
);
|
|
9211
9677
|
}
|
|
9212
|
-
function
|
|
9678
|
+
function pageOptions2(command) {
|
|
9213
9679
|
return command.option("--limit <n>", "Items per page", "25").option("--offset <n>", "Item offset", "0");
|
|
9214
9680
|
}
|
|
9215
9681
|
function sessionOptions(command) {
|
|
@@ -9238,8 +9704,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9238
9704
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9239
9705
|
);
|
|
9240
9706
|
if (opts.outputFile) {
|
|
9241
|
-
const path3 =
|
|
9242
|
-
const existing = await
|
|
9707
|
+
const path3 = resolve5(text(opts.outputFile, "--output-file"));
|
|
9708
|
+
const existing = await lstat4(path3).catch((error) => {
|
|
9243
9709
|
if (error.code !== "ENOENT") throw error;
|
|
9244
9710
|
return void 0;
|
|
9245
9711
|
});
|
|
@@ -9252,9 +9718,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9252
9718
|
numRetries: 0
|
|
9253
9719
|
});
|
|
9254
9720
|
const result = await read(client.dashboard);
|
|
9255
|
-
const output = format === "yaml" ?
|
|
9721
|
+
const output = format === "yaml" ? dump6(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9256
9722
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9257
|
-
await writeReportFile(
|
|
9723
|
+
await writeReportFile(resolve5(String(opts.outputFile)), `${output}
|
|
9258
9724
|
`);
|
|
9259
9725
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9260
9726
|
} else console.log(output);
|
|
@@ -9269,7 +9735,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9269
9735
|
});
|
|
9270
9736
|
}
|
|
9271
9737
|
register6(
|
|
9272
|
-
|
|
9738
|
+
pageOptions2(
|
|
9273
9739
|
timeOptions(
|
|
9274
9740
|
dashboard.command("applications").description("One page of application activity"),
|
|
9275
9741
|
"1",
|
|
@@ -9278,19 +9744,19 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9278
9744
|
)
|
|
9279
9745
|
),
|
|
9280
9746
|
(opts) => {
|
|
9281
|
-
const interval =
|
|
9747
|
+
const interval = integer3(opts.interval, "--interval", 1);
|
|
9282
9748
|
const unit = text(opts.unit, "--unit");
|
|
9283
9749
|
if (![1, 7, 30, 60].includes(interval) || !["day", "days", "hour"].includes(unit))
|
|
9284
9750
|
throw new CliUsageError(
|
|
9285
9751
|
"Application overview supports interval 1/7/30/60 and unit day/days/hour"
|
|
9286
9752
|
);
|
|
9287
|
-
const
|
|
9753
|
+
const query2 = {
|
|
9288
9754
|
timeInterval: interval,
|
|
9289
9755
|
timeUnit: unit,
|
|
9290
|
-
limit:
|
|
9291
|
-
offset:
|
|
9756
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9757
|
+
offset: integer3(opts.offset, "--offset")
|
|
9292
9758
|
};
|
|
9293
|
-
return (client) => client.applicationsOverview(
|
|
9759
|
+
return (client) => client.applicationsOverview(query2);
|
|
9294
9760
|
}
|
|
9295
9761
|
);
|
|
9296
9762
|
for (const [name, method] of [
|
|
@@ -9305,16 +9771,16 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9305
9771
|
["days"]
|
|
9306
9772
|
),
|
|
9307
9773
|
(opts) => {
|
|
9308
|
-
const interval =
|
|
9774
|
+
const interval = integer3(opts.interval, "--interval", 1);
|
|
9309
9775
|
if (![7, 30, 60].includes(interval) || opts.unit !== "days")
|
|
9310
9776
|
throw new CliUsageError("Application detail requires 7, 30, or 60 days");
|
|
9311
|
-
const
|
|
9777
|
+
const query2 = {
|
|
9312
9778
|
appId: text(opts.appId, "--app-id"),
|
|
9313
9779
|
appName: text(opts.appName, "--app-name"),
|
|
9314
9780
|
timeInterval: interval,
|
|
9315
9781
|
timeUnit: "days"
|
|
9316
9782
|
};
|
|
9317
|
-
return (client) => client[method](
|
|
9783
|
+
return (client) => client[method](query2);
|
|
9318
9784
|
}
|
|
9319
9785
|
);
|
|
9320
9786
|
}
|
|
@@ -9326,35 +9792,35 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9326
9792
|
register6(
|
|
9327
9793
|
timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
|
|
9328
9794
|
(opts) => {
|
|
9329
|
-
const
|
|
9330
|
-
return (client) => client[method](
|
|
9795
|
+
const query2 = window(opts);
|
|
9796
|
+
return (client) => client[method](query2);
|
|
9331
9797
|
}
|
|
9332
9798
|
);
|
|
9333
9799
|
}
|
|
9334
9800
|
register6(timeOptions(sessions.command("chart")), (opts) => {
|
|
9335
|
-
const
|
|
9336
|
-
return (client) => client.sessionsChart(
|
|
9801
|
+
const query2 = window(opts);
|
|
9802
|
+
return (client) => client.sessionsChart(query2);
|
|
9337
9803
|
});
|
|
9338
9804
|
register6(
|
|
9339
|
-
|
|
9805
|
+
pageOptions2(
|
|
9340
9806
|
timeOptions(
|
|
9341
9807
|
sessions.command("list").description("Session entries; --all walks pages with completeness checks")
|
|
9342
9808
|
)
|
|
9343
9809
|
).option("--all", "Walk all pages (up to 100 pages)").option("--max <n>", "Item cap with --all; 0 removes item cap", "10000"),
|
|
9344
9810
|
(opts) => {
|
|
9345
|
-
const
|
|
9811
|
+
const query2 = {
|
|
9346
9812
|
...window(opts),
|
|
9347
|
-
limit:
|
|
9348
|
-
offset:
|
|
9813
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9814
|
+
offset: integer3(opts.offset, "--offset")
|
|
9349
9815
|
};
|
|
9350
|
-
const maximum =
|
|
9816
|
+
const maximum = integer3(opts.max, "--max");
|
|
9351
9817
|
return async (client) => {
|
|
9352
9818
|
const items = [];
|
|
9353
9819
|
const seen = /* @__PURE__ */ new Set();
|
|
9354
9820
|
let expectedTotal;
|
|
9355
|
-
let offset =
|
|
9821
|
+
let offset = query2.offset;
|
|
9356
9822
|
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
9357
|
-
const page = await client.sessionsOverview({ ...
|
|
9823
|
+
const page = await client.sessionsOverview({ ...query2, offset });
|
|
9358
9824
|
if (!opts.all) {
|
|
9359
9825
|
ui.status(
|
|
9360
9826
|
`Returned ${page.items.length} of ${page.pagination.total_items ?? "unknown"} sessions; this is one page.`
|
|
@@ -9362,7 +9828,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9362
9828
|
return page.items;
|
|
9363
9829
|
}
|
|
9364
9830
|
const total = page.pagination.total_items;
|
|
9365
|
-
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)
|
|
9366
9832
|
throw new Error("Unstable pagination");
|
|
9367
9833
|
expectedTotal = total;
|
|
9368
9834
|
for (const item of page.items) {
|
|
@@ -9385,7 +9851,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9385
9851
|
}
|
|
9386
9852
|
if (offset === total) {
|
|
9387
9853
|
ui.status(
|
|
9388
|
-
`Retrieved ${items.length} sessions from offset ${
|
|
9854
|
+
`Retrieved ${items.length} sessions from offset ${query2.offset}; pagination complete.`
|
|
9389
9855
|
);
|
|
9390
9856
|
return items;
|
|
9391
9857
|
}
|
|
@@ -9401,33 +9867,33 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9401
9867
|
};
|
|
9402
9868
|
}
|
|
9403
9869
|
);
|
|
9404
|
-
register6(
|
|
9405
|
-
const
|
|
9870
|
+
register6(pageOptions2(sessionOptions(sessions.command("get"))), (opts) => {
|
|
9871
|
+
const query2 = {
|
|
9406
9872
|
...identity(opts),
|
|
9407
|
-
limit:
|
|
9408
|
-
offset:
|
|
9873
|
+
limit: integer3(opts.limit, "--limit", 1),
|
|
9874
|
+
offset: integer3(opts.offset, "--offset")
|
|
9409
9875
|
};
|
|
9410
|
-
return (client) => client.session(
|
|
9876
|
+
return (client) => client.session(query2);
|
|
9411
9877
|
});
|
|
9412
9878
|
register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
|
|
9413
|
-
const
|
|
9414
|
-
return (client) => client.sessionTransaction(
|
|
9879
|
+
const query2 = { ...identity(opts), ...scan(opts) };
|
|
9880
|
+
return (client) => client.sessionTransaction(query2);
|
|
9415
9881
|
});
|
|
9416
9882
|
register6(
|
|
9417
9883
|
scanOptions(
|
|
9418
9884
|
sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
|
|
9419
9885
|
).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
|
|
9420
9886
|
(opts) => {
|
|
9421
|
-
const
|
|
9422
|
-
return (client) => client.scanContent(
|
|
9887
|
+
const query2 = scan(opts);
|
|
9888
|
+
return (client) => client.scanContent(query2);
|
|
9423
9889
|
}
|
|
9424
9890
|
);
|
|
9425
9891
|
}
|
|
9426
9892
|
|
|
9427
9893
|
// src/cli/commands/runtime-report.ts
|
|
9428
|
-
import { lstat as
|
|
9429
|
-
import { resolve as
|
|
9430
|
-
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";
|
|
9431
9897
|
function registerRuntimeReportCommand(runtime) {
|
|
9432
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(
|
|
9433
9899
|
"--output-file <path>",
|
|
@@ -9451,11 +9917,11 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9451
9917
|
usageError("--title must contain 1\u2013240 characters");
|
|
9452
9918
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9453
9919
|
usageError("--output-file cannot be empty");
|
|
9454
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9455
|
-
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);
|
|
9456
9922
|
try {
|
|
9457
9923
|
if (outputPath) {
|
|
9458
|
-
const existing = await
|
|
9924
|
+
const existing = await lstat5(outputPath).catch((error) => {
|
|
9459
9925
|
if (error.code !== "ENOENT") throw error;
|
|
9460
9926
|
return void 0;
|
|
9461
9927
|
});
|
|
@@ -9942,7 +10408,7 @@ function completedBulkResults(state) {
|
|
|
9942
10408
|
}
|
|
9943
10409
|
async function writeBulkResults(outputPath, results) {
|
|
9944
10410
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
9945
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
10411
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID8()}`;
|
|
9946
10412
|
try {
|
|
9947
10413
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
9948
10414
|
encoding: "utf-8",
|
|
@@ -10745,7 +11211,8 @@ var READ_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
|
10745
11211
|
"version",
|
|
10746
11212
|
"versions",
|
|
10747
11213
|
"violation",
|
|
10748
|
-
"violations"
|
|
11214
|
+
"violations",
|
|
11215
|
+
"vulnerabilities"
|
|
10749
11216
|
]);
|
|
10750
11217
|
function applyListDeleteAliases(cmd) {
|
|
10751
11218
|
for (const sub of cmd.commands) {
|
|
@@ -10787,7 +11254,9 @@ function buildProgram() {
|
|
|
10787
11254
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
10788
11255
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
10789
11256
|
setQuiet(Boolean(root.quiet));
|
|
10790
|
-
const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway"
|
|
11257
|
+
const isEnvironmentReport = actionCommand.name() === "report" && ["runtime", "redteam", "aigateway", "agentguard"].includes(
|
|
11258
|
+
actionCommand.parent?.name() ?? ""
|
|
11259
|
+
) && !(actionCommand.parent?.name() === "redteam" && actionCommand.args.length > 0);
|
|
10791
11260
|
if (isEnvironmentReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
10792
11261
|
usageError(
|
|
10793
11262
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
@@ -10803,10 +11272,16 @@ function buildProgram() {
|
|
|
10803
11272
|
if (root.debug) {
|
|
10804
11273
|
const logPath = join4(
|
|
10805
11274
|
process.cwd(),
|
|
10806
|
-
`debug-api-${Date.now()}-${
|
|
11275
|
+
`debug-api-${Date.now()}-${randomUUID9().slice(0, 8)}.jsonl`
|
|
10807
11276
|
);
|
|
10808
11277
|
try {
|
|
10809
|
-
|
|
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 });
|
|
10810
11285
|
} catch {
|
|
10811
11286
|
ui.error(
|
|
10812
11287
|
"Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
|
|
@@ -10819,6 +11294,7 @@ function buildProgram() {
|
|
|
10819
11294
|
registerRuntimeCommand(program);
|
|
10820
11295
|
registerRedteamCommand(program);
|
|
10821
11296
|
registerModelSecurityCommand(program);
|
|
11297
|
+
registerAgentGuardCommand(program);
|
|
10822
11298
|
registerAiGatewayCommand(program);
|
|
10823
11299
|
registerConfigCommand(program);
|
|
10824
11300
|
registerDoctorCommand(program);
|