@cdot65/prisma-airs-cli 4.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -9,18 +9,22 @@ import {
9
9
  SdkPromptSetService,
10
10
  SdkRedTeamService,
11
11
  SdkRuntimeService,
12
+ collectRuntimeDailyReport,
12
13
  computeMetrics,
13
14
  getOrCreateManagementClient,
14
15
  inspectConfig,
15
16
  loadConfig,
16
17
  readBackupDir,
17
18
  readBackupFile,
19
+ renderRuntimeReportHtml,
20
+ renderRuntimeReportMarkdown,
18
21
  resolveConfigFilePath,
19
22
  resolveOutputDir,
20
23
  sanitizeFilename,
21
24
  validateTopic,
22
- writeBackupFile
23
- } from "../chunk-LF6JPUA4.js";
25
+ writeBackupFile,
26
+ writeReportFile
27
+ } from "../chunk-IRLIFMGQ.js";
24
28
 
25
29
  // src/cli/index.ts
26
30
  import "dotenv/config";
@@ -39,8 +43,8 @@ function installProcessGuards() {
39
43
  }
40
44
 
41
45
  // src/cli/program.ts
46
+ import { randomUUID as randomUUID6 } from "crypto";
42
47
  import { readFileSync as readFileSync4 } from "fs";
43
- import { homedir } from "os";
44
48
  import { dirname as dirname4, join as join4 } from "path";
45
49
  import { fileURLToPath } from "url";
46
50
  import { Command } from "commander";
@@ -222,6 +226,17 @@ var SdkAiGatewayService = class {
222
226
  };
223
227
 
224
228
  // src/config/client-options.ts
229
+ function managementClientOptions(config) {
230
+ return {
231
+ clientId: config.mgmtClientId,
232
+ clientSecret: config.mgmtClientSecret,
233
+ tsgId: config.mgmtTsgId,
234
+ apiEndpoint: config.mgmtEndpoint,
235
+ dashboardEndpoint: config.mgmtDashboardEndpoint ?? "https://api.apps.paloaltonetworks.com/aisec",
236
+ tokenEndpoint: config.mgmtTokenEndpoint,
237
+ dlpEndpoint: config.dlpEndpoint
238
+ };
239
+ }
225
240
  function runtimeInitOptions(config) {
226
241
  return {
227
242
  apiKey: config.airsApiKey,
@@ -331,8 +346,8 @@ function displayValue(value) {
331
346
  return typeof value === "object" ? JSON.stringify(value) : String(value);
332
347
  }
333
348
  function csvCell(value) {
334
- const text = displayValue(value);
335
- return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
349
+ const text2 = displayValue(value);
350
+ return /[",\r\n]/.test(text2) ? `"${text2.replace(/"/g, '""')}"` : text2;
336
351
  }
337
352
  function markdownCell(value) {
338
353
  return displayValue(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
@@ -1373,33 +1388,33 @@ function renderMsScanList(scans, format = "pretty") {
1373
1388
  }
1374
1389
  console.log();
1375
1390
  }
1376
- function renderMsScanDetail(scan, format = "pretty") {
1391
+ function renderMsScanDetail(scan2, format = "pretty") {
1377
1392
  if (format !== "pretty") {
1378
- structuredDetail("scan", scan, format, () => void 0);
1393
+ structuredDetail("scan", scan2, format, () => void 0);
1379
1394
  return;
1380
1395
  }
1381
1396
  ui.section("Scan Detail:");
1382
1397
  const pairs = [
1383
- ["UUID", scan.uuid],
1384
- ["Outcome", stateColor(scan.evalOutcome)(scan.evalOutcome)]
1398
+ ["UUID", scan2.uuid],
1399
+ ["Outcome", stateColor(scan2.evalOutcome)(scan2.evalOutcome)]
1385
1400
  ];
1386
- if (scan.modelUri) pairs.push(["Model URI", scan.modelUri]);
1387
- pairs.push(["Origin", scan.scanOrigin]);
1388
- pairs.push(["Source", scan.sourceType]);
1389
- pairs.push(["Group", scan.securityGroupName]);
1390
- pairs.push(["Created", scan.createdAt]);
1391
- pairs.push(["Updated", scan.updatedAt]);
1392
- if (scan.evalSummary) {
1393
- const { rulesPassed, rulesFailed, totalRules } = scan.evalSummary;
1401
+ if (scan2.modelUri) pairs.push(["Model URI", scan2.modelUri]);
1402
+ pairs.push(["Origin", scan2.scanOrigin]);
1403
+ pairs.push(["Source", scan2.sourceType]);
1404
+ pairs.push(["Group", scan2.securityGroupName]);
1405
+ pairs.push(["Created", scan2.createdAt]);
1406
+ pairs.push(["Updated", scan2.updatedAt]);
1407
+ if (scan2.evalSummary) {
1408
+ const { rulesPassed, rulesFailed, totalRules } = scan2.evalSummary;
1394
1409
  pairs.push([
1395
1410
  "Rules",
1396
1411
  `${chalk7.green(`${rulesPassed} passed`)} ${chalk7.red(`${rulesFailed} failed`)} / ${totalRules} total`
1397
1412
  ]);
1398
1413
  }
1399
1414
  ui.keyValue(pairs);
1400
- if (scan.labels.length > 0) {
1415
+ if (scan2.labels.length > 0) {
1401
1416
  ui.section("Labels:");
1402
- ui.keyValue(scan.labels.map((l) => [l.key, l.value]));
1417
+ ui.keyValue(scan2.labels.map((l) => [l.key, l.value]));
1403
1418
  }
1404
1419
  console.log();
1405
1420
  }
@@ -2084,9 +2099,9 @@ function renderPromptList(prompts, format = "pretty") {
2084
2099
  ui.section("Prompts:");
2085
2100
  for (const p of prompts) {
2086
2101
  const status = p.active ? chalk8.green("active") : chalk8.dim("inactive");
2087
- const text = p.prompt.length > 80 ? `${p.prompt.substring(0, 77)}...` : p.prompt;
2102
+ const text2 = p.prompt.length > 80 ? `${p.prompt.substring(0, 77)}...` : p.prompt;
2088
2103
  console.log(` ${chalk8.dim(p.uuid)} ${status}`);
2089
- console.log(` ${text}`);
2104
+ console.log(` ${text2}`);
2090
2105
  if (p.goal) console.log(` ${chalk8.dim(`Goal: ${p.goal}`)}`);
2091
2106
  }
2092
2107
  console.log();
@@ -2900,54 +2915,6 @@ function renderDeploymentProfileList(profiles, format = "pretty") {
2900
2915
  }
2901
2916
  console.log();
2902
2917
  }
2903
- function renderScanLogList(results, pageToken, format = "pretty") {
2904
- if (results.length === 0) {
2905
- if (format === "pretty") ui.emptyList("scan logs");
2906
- else console.log(formatOutput([], [], format));
2907
- return;
2908
- }
2909
- if (format !== "pretty") {
2910
- const rows = results.map((r) => ({
2911
- scanId: r.scan_id ?? "",
2912
- timestamp: r.received_ts ?? r.timestamp ?? "",
2913
- action: r.action ?? r.verdict ?? "",
2914
- profile: r.profile_name ?? "",
2915
- app: r.app_name ?? ""
2916
- }));
2917
- console.log(
2918
- formatOutput(
2919
- rows,
2920
- [
2921
- { key: "scanId", label: "Scan ID" },
2922
- { key: "timestamp", label: "Timestamp" },
2923
- { key: "action", label: "Action" },
2924
- { key: "profile", label: "Profile" },
2925
- { key: "app", label: "App" }
2926
- ],
2927
- format
2928
- )
2929
- );
2930
- return;
2931
- }
2932
- ui.section(`Scan Logs (${results.length} results):`);
2933
- for (const r of results) {
2934
- const action = r.action ?? r.verdict;
2935
- const app = r.app_name;
2936
- const profile = r.profile_name;
2937
- const ts2 = r.received_ts ?? r.timestamp;
2938
- const scanId = r.scan_id;
2939
- const actionColor = action === "block" ? chalk9.red : chalk9.green;
2940
- if (scanId) ui.dim(scanId);
2941
- console.log(
2942
- ` ${ts2 ? chalk9.dim(ts2) : ""} ${action ? actionColor(action) : ""} ${profile ? `[${profile}]` : ""} ${app ?? ""}`
2943
- );
2944
- }
2945
- if (pageToken) {
2946
- console.log();
2947
- ui.dim(`Page token: ${pageToken}`);
2948
- }
2949
- console.log();
2950
- }
2951
2918
 
2952
2919
  // src/cli/confirm.ts
2953
2920
  async function confirmOrAbort(message, force, options = {}) {
@@ -3085,8 +3052,8 @@ function registerAiGatewayInference(parent) {
3085
3052
  process.once("SIGINT", interrupt);
3086
3053
  process.once("SIGTERM", interrupt);
3087
3054
  process.stdout.on("error", outputError);
3088
- const write = (text) => new Promise((resolve2, reject) => {
3089
- process.stdout.write(text, (error) => error ? reject(error) : resolve2());
3055
+ const write = (text2) => new Promise((resolve4, reject) => {
3056
+ process.stdout.write(text2, (error) => error ? reject(error) : resolve4());
3090
3057
  });
3091
3058
  const requestOptions = { signal: controller.signal };
3092
3059
  const result = kind === "chat" ? await client.createChatCompletion(
@@ -3105,16 +3072,16 @@ function registerAiGatewayInference(parent) {
3105
3072
  if (format === "json") await write(`${JSON.stringify(event)}
3106
3073
  `);
3107
3074
  else {
3108
- const text = responseText(event);
3109
- if (text) await write(text);
3075
+ const text2 = responseText(event);
3076
+ if (text2) await write(text2);
3110
3077
  }
3111
3078
  if ("type" in event && (event.type === "response.failed" || event.type === "response.incomplete"))
3112
3079
  throw new Error(`Gateway generation ended with ${event.type}`);
3113
3080
  }
3114
3081
  if (format === "pretty") await write("\n");
3115
3082
  } else {
3116
- const text = format === "yaml" ? dump3(result, { noRefs: true }).trimEnd() : format === "pretty" && kind !== "embeddings" ? responseText(result) : JSON.stringify(result, null, 2);
3117
- await write(`${text}
3083
+ const text2 = format === "yaml" ? dump3(result, { noRefs: true }).trimEnd() : format === "pretty" && kind !== "embeddings" ? responseText(result) : JSON.stringify(result, null, 2);
3084
+ await write(`${text2}
3118
3085
  `);
3119
3086
  if ("status" in result && (result.status === "failed" || result.status === "incomplete"))
3120
3087
  throw new Error(`Gateway generation ended with ${result.status}`);
@@ -3184,6 +3151,7 @@ import {
3184
3151
  import { dirname, join } from "path";
3185
3152
  var AIRS_DOMAINS = [
3186
3153
  "api.sase.paloaltonetworks.com",
3154
+ "api.apps.paloaltonetworks.com",
3187
3155
  "service.api.aisecurity.paloaltonetworks.com",
3188
3156
  "auth.apps.paloaltonetworks.com",
3189
3157
  "api.dlp.paloaltonetworks.com"
@@ -3197,7 +3165,7 @@ function isAirsUrl(url) {
3197
3165
  }
3198
3166
  }
3199
3167
  var MASK = "***";
3200
- var SENSITIVE_KEY_PATTERN = /token|secret|password|passwd|credential|authorization|cookie|api[-_]?key|client[-_]?auth|^key$/i;
3168
+ var SENSITIVE_KEY_PATTERN = /token|secret|password|passwd|credential|authorization|cookie|api[-_]?key|client[-_]?auth|auth[-_]?code|^key$/i;
3201
3169
  function isSensitiveKey(key) {
3202
3170
  return SENSITIVE_KEY_PATTERN.test(key);
3203
3171
  }
@@ -3236,28 +3204,6 @@ function redactUrl(url) {
3236
3204
  return url;
3237
3205
  }
3238
3206
  }
3239
- function pruneDebugLogs(dir, keep) {
3240
- let files;
3241
- try {
3242
- files = readdirSync(dir).filter((f) => f.startsWith("debug-api-") && f.endsWith(".jsonl"));
3243
- } catch {
3244
- return;
3245
- }
3246
- const byAge = files.map((f) => {
3247
- const path3 = join(dir, f);
3248
- try {
3249
- return { path: path3, mtime: statSync(path3).mtimeMs };
3250
- } catch {
3251
- return null;
3252
- }
3253
- }).filter((e) => e !== null).sort((a, b) => b.mtime - a.mtime);
3254
- for (const { path: path3 } of byAge.slice(keep)) {
3255
- try {
3256
- unlinkSync(path3);
3257
- } catch {
3258
- }
3259
- }
3260
- }
3261
3207
  function headersToRecord(headers) {
3262
3208
  if (!headers) return {};
3263
3209
  if (typeof headers === "object" && "forEach" in headers && typeof headers.forEach === "function") {
@@ -3272,11 +3218,9 @@ function headersToRecord(headers) {
3272
3218
  }
3273
3219
  return headers;
3274
3220
  }
3275
- var KEEP_DEBUG_LOGS = 10;
3276
3221
  function installDebugLogger(logPath) {
3277
3222
  mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
3278
- writeFileSync(logPath, "", { encoding: "utf8", mode: 384 });
3279
- pruneDebugLogs(dirname(logPath), KEEP_DEBUG_LOGS);
3223
+ writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
3280
3224
  const originalFetch = globalThis.fetch;
3281
3225
  globalThis.fetch = async function debugFetch(input, init2) {
3282
3226
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -3289,16 +3233,18 @@ function installDebugLogger(logPath) {
3289
3233
  if (!isAirsUrl(url) && !inference) {
3290
3234
  return originalFetch(input, init2);
3291
3235
  }
3236
+ const dashboard = /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url);
3292
3237
  const method = init2?.method ?? (input instanceof Request ? input.method : "GET");
3293
3238
  const reqHeaders = redactHeaders(rawHeaders);
3294
3239
  const loggedUrl = redactUrl(url);
3295
3240
  let reqBody;
3296
- if (inference) reqBody = "[BODY OMITTED]";
3241
+ if (inference || dashboard) reqBody = "[BODY OMITTED]";
3297
3242
  else if (init2?.body) {
3298
3243
  try {
3299
3244
  reqBody = redactDeep(JSON.parse(String(init2.body)));
3300
3245
  } catch {
3301
- reqBody = String(init2.body);
3246
+ const contentType = new Headers(rawHeaders).get("content-type") ?? "";
3247
+ reqBody = contentType.includes("application/x-www-form-urlencoded") ? redactDeep(Object.fromEntries(new URLSearchParams(String(init2.body)))) : "[NON-JSON BODY OMITTED]";
3302
3248
  }
3303
3249
  }
3304
3250
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
@@ -3309,7 +3255,7 @@ function installDebugLogger(logPath) {
3309
3255
  try {
3310
3256
  response = await originalFetch(input, init2);
3311
3257
  } catch (err) {
3312
- error = inference ? "Runtime request failed" : err instanceof Error ? err.message : String(err);
3258
+ error = inference ? "Runtime request failed" : "Request failed";
3313
3259
  const entry2 = JSON.stringify({
3314
3260
  timestamp: ts2,
3315
3261
  durationMs: Date.now() - startMs,
@@ -3325,16 +3271,16 @@ function installDebugLogger(logPath) {
3325
3271
  response.headers.forEach((v, k) => {
3326
3272
  resHeaders[k] = v;
3327
3273
  });
3328
- if (inference || response.headers.get("content-type")?.includes("text/event-stream")) {
3274
+ if (inference || dashboard || response.headers.get("content-type")?.includes("text/event-stream")) {
3329
3275
  resBody = "[BODY OMITTED]";
3330
3276
  } else {
3331
3277
  const clone = response.clone();
3332
3278
  try {
3333
- const text = await clone.text();
3279
+ const text2 = await clone.text();
3334
3280
  try {
3335
- resBody = redactDeep(JSON.parse(text));
3281
+ resBody = redactDeep(JSON.parse(text2));
3336
3282
  } catch {
3337
- resBody = text;
3283
+ resBody = "[NON-JSON BODY OMITTED]";
3338
3284
  }
3339
3285
  } catch {
3340
3286
  resBody = "<unreadable>";
@@ -3389,8 +3335,8 @@ function addWriteOutput(command) {
3389
3335
  async function readGatewayRequest(path3) {
3390
3336
  let parsed;
3391
3337
  try {
3392
- const text = await readFile2(path3, "utf8");
3393
- parsed = path3.endsWith(".yaml") || path3.endsWith(".yml") ? load(text) : JSON.parse(text);
3338
+ const text2 = await readFile2(path3, "utf8");
3339
+ parsed = path3.endsWith(".yaml") || path3.endsWith(".yml") ? load(text2) : JSON.parse(text2);
3394
3340
  } catch (error) {
3395
3341
  throw new CliUsageError(
3396
3342
  `Unable to parse --file '${path3}' as JSON or YAML: ${error instanceof Error ? error.message : String(error)}`
@@ -3605,9 +3551,9 @@ function parseBooleanOption(value) {
3605
3551
  throw new CliUsageError("Expected true or false");
3606
3552
  }
3607
3553
  function parseIntegerOption(value) {
3608
- const text = String(value);
3609
- if (!/^-?(0|[1-9]\d*)$/.test(text)) throw new CliUsageError("Expected an integer");
3610
- const parsed = Number(text);
3554
+ const text2 = String(value);
3555
+ if (!/^-?(0|[1-9]\d*)$/.test(text2)) throw new CliUsageError("Expected an integer");
3556
+ const parsed = Number(text2);
3611
3557
  if (!Number.isSafeInteger(parsed)) throw new CliUsageError("Expected a safe integer");
3612
3558
  return parsed;
3613
3559
  }
@@ -3651,14 +3597,14 @@ function parseModelBindingsOption(value) {
3651
3597
  }
3652
3598
  function parseCapabilityBindingsOption(value) {
3653
3599
  return repeatableValues(value).map((raw) => {
3654
- const [identity, enabled] = splitKeyValue(raw, "type:name");
3655
- const separator = identity.indexOf(":");
3656
- if (separator <= 0 || separator === identity.length - 1) {
3600
+ const [identity2, enabled] = splitKeyValue(raw, "type:name");
3601
+ const separator = identity2.indexOf(":");
3602
+ if (separator <= 0 || separator === identity2.length - 1) {
3657
3603
  throw new CliUsageError("Expected type:name=enabled");
3658
3604
  }
3659
3605
  return {
3660
- type: identity.slice(0, separator),
3661
- name: identity.slice(separator + 1),
3606
+ type: identity2.slice(0, separator),
3607
+ name: identity2.slice(separator + 1),
3662
3608
  enabled: parseBooleanOption(enabled)
3663
3609
  };
3664
3610
  });
@@ -3679,12 +3625,12 @@ function parseAssignment(raw, forceString) {
3679
3625
  const separator = raw.indexOf("=");
3680
3626
  if (separator <= 0) throw new CliUsageError("Expected path=value");
3681
3627
  const path3 = raw.slice(0, separator);
3682
- const text = raw.slice(separator + 1);
3683
- if (forceString) return [path3, text];
3628
+ const text2 = raw.slice(separator + 1);
3629
+ if (forceString) return [path3, text2];
3684
3630
  try {
3685
- return [path3, JSON.parse(text)];
3631
+ return [path3, JSON.parse(text2)];
3686
3632
  } catch {
3687
- return [path3, text];
3633
+ return [path3, text2];
3688
3634
  }
3689
3635
  }
3690
3636
  function schemaMessage(error) {
@@ -4608,11 +4554,11 @@ function addWindowOptions(command) {
4608
4554
  );
4609
4555
  }
4610
4556
  function windowFrom(opts) {
4611
- const window = { workspaceSlug: opts.workspace };
4612
- if (opts.start) window.start = parseNamedDate2(opts.start, "--start");
4613
- else window.days = parsePositiveInteger2(opts.days ?? "7", "--days");
4614
- if (opts.end) window.end = parseNamedDate2(opts.end, "--end");
4615
- return window;
4557
+ const window2 = { workspaceSlug: opts.workspace };
4558
+ if (opts.start) window2.start = parseNamedDate2(opts.start, "--start");
4559
+ else window2.days = parsePositiveInteger2(opts.days ?? "7", "--days");
4560
+ if (opts.end) window2.end = parseNamedDate2(opts.end, "--end");
4561
+ return window2;
4616
4562
  }
4617
4563
  function parsePositiveInteger2(value, flag) {
4618
4564
  try {
@@ -5350,8 +5296,8 @@ async function withTimeout(promise, ms) {
5350
5296
  try {
5351
5297
  return await Promise.race([
5352
5298
  promise,
5353
- new Promise((resolve2) => {
5354
- timer = setTimeout(() => resolve2(TIMED_OUT), ms);
5299
+ new Promise((resolve4) => {
5300
+ timer = setTimeout(() => resolve4(TIMED_OUT), ms);
5355
5301
  })
5356
5302
  ]);
5357
5303
  } finally {
@@ -5621,12 +5567,12 @@ function hasBin(bin) {
5621
5567
  }
5622
5568
  }
5623
5569
  function run(bin, args, label) {
5624
- return new Promise((resolve2, reject) => {
5570
+ return new Promise((resolve4, reject) => {
5625
5571
  const child = spawn(bin, args, { stdio: "inherit" });
5626
5572
  child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
5627
5573
  child.on("close", (code) => {
5628
5574
  if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
5629
- else resolve2();
5575
+ else resolve4();
5630
5576
  });
5631
5577
  });
5632
5578
  }
@@ -5947,8 +5893,8 @@ function registerModelSecurityCommand(program) {
5947
5893
  const fmt = opts.output;
5948
5894
  if (fmt === "pretty") renderModelSecurityHeader();
5949
5895
  const service = await createService2();
5950
- const scan = await service.getScan(uuid);
5951
- renderMsScanDetail(scan, fmt);
5896
+ const scan2 = await service.getScan(uuid);
5897
+ renderMsScanDetail(scan2, fmt);
5952
5898
  } catch (err) {
5953
5899
  fail(err);
5954
5900
  }
@@ -5958,9 +5904,9 @@ function registerModelSecurityCommand(program) {
5958
5904
  renderModelSecurityHeader();
5959
5905
  const service = await createService2();
5960
5906
  const config = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
5961
- const scan = await service.createScan(config);
5962
- ui.success(`Scan created: ${scan.uuid}`);
5963
- renderMsScanDetail(scan);
5907
+ const scan2 = await service.createScan(config);
5908
+ ui.success(`Scan created: ${scan2.uuid}`);
5909
+ renderMsScanDetail(scan2);
5964
5910
  } catch (err) {
5965
5911
  fail(err);
5966
5912
  }
@@ -7439,7 +7385,7 @@ function registerRedteamCommand(program) {
7439
7385
  }
7440
7386
 
7441
7387
  // src/cli/commands/runtime.ts
7442
- import { randomUUID as randomUUID4 } from "crypto";
7388
+ import { randomUUID as randomUUID5 } from "crypto";
7443
7389
  import * as fs5 from "fs";
7444
7390
  import { readFile as readFile10 } from "fs/promises";
7445
7391
  import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
@@ -7507,7 +7453,7 @@ function buildDataProtection(flags) {
7507
7453
  const dld = { action: flags.dlpAction };
7508
7454
  const profiles = parseList(flags.dlpProfiles);
7509
7455
  if (profiles) {
7510
- dld.member = profiles.map((text) => ({ text }));
7456
+ dld.member = profiles.map((text2) => ({ text: text2 }));
7511
7457
  }
7512
7458
  if (flags.maskDataInline != null) {
7513
7459
  dld["mask-data-inline"] = flags.maskDataInline;
@@ -8994,6 +8940,320 @@ function registerCleanupCommand(parent) {
8994
8940
  });
8995
8941
  }
8996
8942
 
8943
+ // src/cli/commands/runtime-dashboard.ts
8944
+ import { lstat } from "fs/promises";
8945
+ import { resolve as resolve2 } from "path";
8946
+ import { dump as dump5 } from "js-yaml";
8947
+ function integer2(value, name, minimum = 0) {
8948
+ const n = Number(value);
8949
+ if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(n) || n < minimum)
8950
+ throw new CliUsageError(`${name} must be a safe integer >= ${minimum}`);
8951
+ return n;
8952
+ }
8953
+ function text(value, name) {
8954
+ if (typeof value !== "string" || !value.trim()) throw new CliUsageError(`${name} is required`);
8955
+ return value;
8956
+ }
8957
+ function window(opts) {
8958
+ return {
8959
+ timeInterval: integer2(opts.interval, "--interval", 1),
8960
+ timeUnit: text(opts.unit, "--unit")
8961
+ };
8962
+ }
8963
+ function identity(opts) {
8964
+ return {
8965
+ sessionId: text(opts.sessionId, "--session-id"),
8966
+ appId: text(opts.appId, "--app-id"),
8967
+ appName: text(opts.appName, "--app-name"),
8968
+ ...window(opts)
8969
+ };
8970
+ }
8971
+ function scan(opts) {
8972
+ return {
8973
+ scanId: text(opts.scanId, "--scan-id"),
8974
+ scanSubReqId: integer2(opts.scanSubReqId, "--scan-sub-req-id")
8975
+ };
8976
+ }
8977
+ function timeOptions(command, interval = "1", unit = "day") {
8978
+ return command.option("--interval <n>", "Look-back interval", interval).option("--unit <unit>", "API time unit (windows are deployment-dependent)", unit);
8979
+ }
8980
+ function pageOptions(command) {
8981
+ return command.option("--limit <n>", "Items per page", "25").option("--offset <n>", "Item offset", "0");
8982
+ }
8983
+ function sessionOptions(command) {
8984
+ return timeOptions(
8985
+ command.requiredOption("--session-id <id>", "Session identity").requiredOption("--app-id <id>", "Application identity").requiredOption("--app-name <name>", "Exact dashboard application name"),
8986
+ "30",
8987
+ "days"
8988
+ );
8989
+ }
8990
+ function scanOptions(command) {
8991
+ return command.requiredOption("--scan-id <id>", "Scan identity").requiredOption("--scan-sub-req-id <n>", "Sub-request index, including zero");
8992
+ }
8993
+ function registerRuntimeDashboardCommands(runtime) {
8994
+ const dashboard = runtime.command("dashboard").description("SCM application summaries and daily telemetry (OAuth)");
8995
+ const sessions = runtime.command("sessions").description("Verified SCM session inventory and drill-down (OAuth)");
8996
+ function register6(command, prepare) {
8997
+ command.option("--output <format>", "Output format: pretty, json, yaml").action(async (opts) => {
8998
+ try {
8999
+ const read = prepare(opts);
9000
+ const format = await resolveOutput(command, opts, {
9001
+ allowed: ["pretty", "json", "yaml"]
9002
+ });
9003
+ if (command.name() === "scan-content") {
9004
+ if (Boolean(opts.showContent) === Boolean(opts.outputFile))
9005
+ throw new CliUsageError(
9006
+ "Scan content requires exactly one of --show-content or --output-file <new-path>"
9007
+ );
9008
+ if (opts.outputFile) {
9009
+ const path3 = resolve2(text(opts.outputFile, "--output-file"));
9010
+ const existing = await lstat(path3).catch((error) => {
9011
+ if (error.code !== "ENOENT") throw error;
9012
+ return void 0;
9013
+ });
9014
+ if (existing) throw new Error("Destination exists");
9015
+ }
9016
+ }
9017
+ const config = await loadConfig();
9018
+ const client = getOrCreateManagementClient({
9019
+ ...managementClientOptions(config),
9020
+ numRetries: 0
9021
+ });
9022
+ const result = await read(client.dashboard);
9023
+ const output = format === "yaml" ? dump5(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
9024
+ if (command.name() === "scan-content" && opts.outputFile) {
9025
+ await writeReportFile(resolve2(String(opts.outputFile)), `${output}
9026
+ `);
9027
+ ui.status("Scan content exported to the requested private file (0600).");
9028
+ } else console.log(output);
9029
+ } catch (error) {
9030
+ if (error instanceof CliUsageError) usageError(error.message);
9031
+ const status = error?.statusCode;
9032
+ ui.error(
9033
+ `Dashboard operation failed${typeof status === "number" && status >= 400 && status <= 599 ? ` (HTTP ${status})` : ""}. Check credentials, query options, and any output destination. No existing file is overwritten.`
9034
+ );
9035
+ process.exitCode = 1;
9036
+ }
9037
+ });
9038
+ }
9039
+ register6(
9040
+ pageOptions(
9041
+ timeOptions(
9042
+ dashboard.command("applications").description("One page of application activity")
9043
+ )
9044
+ ),
9045
+ (opts) => {
9046
+ const interval = integer2(opts.interval, "--interval", 1);
9047
+ const unit = text(opts.unit, "--unit");
9048
+ if (![1, 7, 30, 60].includes(interval) || !["day", "days", "hour"].includes(unit))
9049
+ throw new CliUsageError(
9050
+ "Application overview supports interval 1/7/30/60 and unit day/days/hour"
9051
+ );
9052
+ const query = {
9053
+ timeInterval: interval,
9054
+ timeUnit: unit,
9055
+ limit: integer2(opts.limit, "--limit", 1),
9056
+ offset: integer2(opts.offset, "--offset")
9057
+ };
9058
+ return (client) => client.applicationsOverview(query);
9059
+ }
9060
+ );
9061
+ for (const [name, method] of [
9062
+ ["application", "application"],
9063
+ ["application-violations", "applicationViolationBreakdown"]
9064
+ ]) {
9065
+ register6(
9066
+ timeOptions(
9067
+ dashboard.command(name).requiredOption("--app-id <id>", "Application identity").requiredOption("--app-name <name>", "Exact dashboard application name"),
9068
+ "30",
9069
+ "days"
9070
+ ),
9071
+ (opts) => {
9072
+ const interval = integer2(opts.interval, "--interval", 1);
9073
+ if (![7, 30, 60].includes(interval) || opts.unit !== "days")
9074
+ throw new CliUsageError("Application detail requires 7, 30, or 60 days");
9075
+ const query = {
9076
+ appId: text(opts.appId, "--app-id"),
9077
+ appName: text(opts.appName, "--app-name"),
9078
+ timeInterval: interval,
9079
+ timeUnit: "days"
9080
+ };
9081
+ return (client) => client[method](query);
9082
+ }
9083
+ );
9084
+ }
9085
+ for (const [name, method, days] of [
9086
+ ["top-applications", "topApplicationsViolations", false],
9087
+ ["violations-trend", "applicationsViolationsTrend", false],
9088
+ ["apps-list", "appsList", true]
9089
+ ]) {
9090
+ register6(
9091
+ timeOptions(dashboard.command(name), days ? "30" : "1", days ? "days" : "day"),
9092
+ (opts) => {
9093
+ const query = window(opts);
9094
+ return (client) => client[method](query);
9095
+ }
9096
+ );
9097
+ }
9098
+ register6(timeOptions(sessions.command("chart")), (opts) => {
9099
+ const query = window(opts);
9100
+ return (client) => client.sessionsChart(query);
9101
+ });
9102
+ register6(
9103
+ pageOptions(
9104
+ timeOptions(
9105
+ sessions.command("list").description("Session entries; --all walks pages with completeness checks")
9106
+ )
9107
+ ).option("--all", "Walk all pages (up to 100 pages)").option("--max <n>", "Item cap with --all; 0 removes item cap", "10000"),
9108
+ (opts) => {
9109
+ const query = {
9110
+ ...window(opts),
9111
+ limit: integer2(opts.limit, "--limit", 1),
9112
+ offset: integer2(opts.offset, "--offset")
9113
+ };
9114
+ const maximum = integer2(opts.max, "--max");
9115
+ return async (client) => {
9116
+ const items = [];
9117
+ const seen = /* @__PURE__ */ new Set();
9118
+ let expectedTotal;
9119
+ let offset = query.offset;
9120
+ for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
9121
+ const page = await client.sessionsOverview({ ...query, offset });
9122
+ if (!opts.all) {
9123
+ ui.status(
9124
+ `Returned ${page.items.length} of ${page.pagination.total_items ?? "unknown"} sessions; this is one page.`
9125
+ );
9126
+ return page.items;
9127
+ }
9128
+ const total = page.pagination.total_items;
9129
+ if (!Number.isSafeInteger(total) || total < 0 || page.pagination.skip !== offset || page.pagination.limit !== query.limit || page.items.length > query.limit || expectedTotal !== void 0 && total !== expectedTotal)
9130
+ throw new Error("Unstable pagination");
9131
+ expectedTotal = total;
9132
+ for (const item of page.items) {
9133
+ const key = JSON.stringify([
9134
+ item.application_id,
9135
+ item.application_name,
9136
+ item.session_id
9137
+ ]);
9138
+ if (seen.has(key)) throw new Error("Overlapping session pages");
9139
+ seen.add(key);
9140
+ items.push(item);
9141
+ }
9142
+ offset += page.items.length;
9143
+ if (offset > total || page.items.length === 0 && offset < total)
9144
+ throw new Error("Incomplete inventory");
9145
+ if (maximum > 0 && items.length > maximum) {
9146
+ ui.status("Session item cap reached; output is partial.");
9147
+ process.exitCode = 1;
9148
+ return items.slice(0, maximum);
9149
+ }
9150
+ if (offset === total) {
9151
+ ui.status(
9152
+ `Retrieved ${items.length} sessions from offset ${query.offset}; pagination complete.`
9153
+ );
9154
+ return items;
9155
+ }
9156
+ if (maximum > 0 && items.length === maximum) {
9157
+ ui.status("Session item cap reached; output is partial.");
9158
+ process.exitCode = 1;
9159
+ return items;
9160
+ }
9161
+ }
9162
+ ui.status("Session page cap reached; output is partial.");
9163
+ process.exitCode = 1;
9164
+ return items;
9165
+ };
9166
+ }
9167
+ );
9168
+ register6(pageOptions(sessionOptions(sessions.command("get"))), (opts) => {
9169
+ const query = {
9170
+ ...identity(opts),
9171
+ limit: integer2(opts.limit, "--limit", 1),
9172
+ offset: integer2(opts.offset, "--offset")
9173
+ };
9174
+ return (client) => client.session(query);
9175
+ });
9176
+ register6(scanOptions(sessionOptions(sessions.command("transaction"))), (opts) => {
9177
+ const query = { ...identity(opts), ...scan(opts) };
9178
+ return (client) => client.sessionTransaction(query);
9179
+ });
9180
+ register6(
9181
+ scanOptions(
9182
+ sessions.command("scan-content").description("Explicit sensitive scan-content read; never automatically fetched")
9183
+ ).option("--show-content", "Explicitly display stored content on stdout").option("--output-file <path>", "Private new file for stored content (no overwrite)"),
9184
+ (opts) => {
9185
+ const query = scan(opts);
9186
+ return (client) => client.scanContent(query);
9187
+ }
9188
+ );
9189
+ }
9190
+
9191
+ // src/cli/commands/runtime-report.ts
9192
+ import { lstat as lstat2 } from "fs/promises";
9193
+ import { resolve as resolve3 } from "path";
9194
+ import { randomUUID as randomUUID4 } from "crypto";
9195
+ function registerRuntimeReportCommand(runtime) {
9196
+ const command = runtime.command("report").description("Generate a daily AI Runtime Security environment dashboard (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
9197
+ "--output-file <path>",
9198
+ "Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
9199
+ ).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(
9200
+ "after",
9201
+ examples(
9202
+ "airs runtime report --output-file ./airs-daily.html",
9203
+ "airs runtime report --output markdown --output-file ./airs-daily.md",
9204
+ "airs runtime report",
9205
+ "airs runtime report --strict --max-pages 20 --output-file - > daily.html"
9206
+ )
9207
+ ).action(async (opts) => {
9208
+ const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
9209
+ if (format !== "html" && format !== "markdown")
9210
+ usageError("Report output must be html or markdown");
9211
+ const maxPages = Number(opts.maxPages);
9212
+ if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
9213
+ usageError("--max-pages must be an integer from 1 to 100");
9214
+ if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
9215
+ usageError("--title must contain 1\u2013240 characters");
9216
+ if (opts.outputFile !== void 0 && !opts.outputFile.trim())
9217
+ usageError("--output-file cannot be empty");
9218
+ const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID4().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
9219
+ const outputPath = opts.outputFile === "-" ? void 0 : resolve3(opts.outputFile ?? defaultName);
9220
+ try {
9221
+ if (outputPath) {
9222
+ const existing = await lstat2(outputPath).catch((error) => {
9223
+ if (error.code !== "ENOENT") throw error;
9224
+ return void 0;
9225
+ });
9226
+ if (existing) throw new Error("Output already exists");
9227
+ }
9228
+ const config = await loadConfig();
9229
+ const client = getOrCreateManagementClient({
9230
+ ...managementClientOptions(config),
9231
+ numRetries: 0
9232
+ });
9233
+ ui.status("Collecting daily runtime activity and current configuration (read-only)...");
9234
+ const report = await collectRuntimeDailyReport(client, { title: opts.title, maxPages });
9235
+ const rendered = format === "html" ? renderRuntimeReportHtml(report) : renderRuntimeReportMarkdown(report);
9236
+ if (outputPath) {
9237
+ await writeReportFile(outputPath, rendered);
9238
+ ui.status(`Report \u2192 ${outputPath.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
9239
+ } else {
9240
+ await new Promise(
9241
+ (done, reject) => process.stdout.write(rendered, (error) => error ? reject(error) : done())
9242
+ );
9243
+ }
9244
+ const incomplete = report.sources.filter((source) => source.status !== "complete").length;
9245
+ if (incomplete)
9246
+ ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
9247
+ if (report.health === "unknown" || opts.strict && incomplete > 0) process.exitCode = 1;
9248
+ } catch {
9249
+ ui.error(
9250
+ "Report generation failed. Verify Management API configuration and a writable, new output path. Existing files are never overwritten."
9251
+ );
9252
+ process.exitCode = 1;
9253
+ }
9254
+ });
9255
+ }
9256
+
8997
9257
  // src/cli/commands/topics-apply.ts
8998
9258
  async function applyTopicToProfile(mgmt, input) {
8999
9259
  const allTopics = await mgmt.listTopics();
@@ -9242,14 +9502,14 @@ async function evalTopic(scanner, profileName, topicName, cases, concurrency = 5
9242
9502
  throw new Error(`scanBatch returned ${scanResults.length} results for ${cases.length} prompts`);
9243
9503
  }
9244
9504
  const testResults = cases.map((testCase, i) => {
9245
- const scan = scanResults[i];
9505
+ const scan2 = scanResults[i];
9246
9506
  return {
9247
9507
  testCase,
9248
- actualTriggered: scan.triggered,
9249
- scanAction: scan.action,
9250
- scanId: scan.scanId,
9251
- reportId: scan.reportId,
9252
- correct: testCase.expectedTriggered === scan.triggered
9508
+ actualTriggered: scan2.triggered,
9509
+ scanAction: scan2.action,
9510
+ scanId: scan2.scanId,
9511
+ reportId: scan2.reportId,
9512
+ correct: testCase.expectedTriggered === scan2.triggered
9253
9513
  };
9254
9514
  });
9255
9515
  const metrics = computeMetrics(testResults);
@@ -9446,7 +9706,7 @@ function completedBulkResults(state) {
9446
9706
  }
9447
9707
  async function writeBulkResults(outputPath, results) {
9448
9708
  await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
9449
- const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID4()}`;
9709
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID5()}`;
9450
9710
  try {
9451
9711
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
9452
9712
  encoding: "utf-8",
@@ -9472,15 +9732,12 @@ function parsePositiveInteger3(value, optionName2) {
9472
9732
  }
9473
9733
  async function createMgmtService() {
9474
9734
  const config = await loadConfig();
9475
- return new SdkManagementService({
9476
- clientId: config.mgmtClientId,
9477
- clientSecret: config.mgmtClientSecret,
9478
- tsgId: config.mgmtTsgId,
9479
- tokenEndpoint: config.mgmtTokenEndpoint
9480
- });
9735
+ return new SdkManagementService(managementClientOptions(config));
9481
9736
  }
9482
9737
  function registerRuntimeCommand(program) {
9483
9738
  const runtime = program.command("runtime").description("Runtime prompt scanning against AIRS profiles");
9739
+ registerRuntimeReportCommand(runtime);
9740
+ registerRuntimeDashboardCommands(runtime);
9484
9741
  const apiKeys = runtime.command("api-keys").description("Manage AIRS API keys");
9485
9742
  const apiKeysList = registerListFlags(apiKeys.command("list"), { dialect: "offset" }).description("List API keys").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (opts) => {
9486
9743
  try {
@@ -10139,26 +10396,14 @@ function registerRuntimeCommand(program) {
10139
10396
  fail(err);
10140
10397
  }
10141
10398
  });
10142
- const scanLogs = runtime.command("scan-logs").description("Query AIRS scan logs");
10143
- const scanLogsQuery = scanLogs.command("query").description("Query scan logs").requiredOption("--interval <n>", "Time interval").requiredOption("--unit <unit>", "Time unit (hours)").option("--filter <filter>", "Filter: all, benign, threat", "all").option("--limit <n>", "Max results per page (API page size)", "50").option("--offset <n>", "Starting offset \u2014 rounds down to a page boundary", "0").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml");
10399
+ const scanLogs = runtime.command("scan-logs").description("BROKEN legacy retrieval \u2014 under refactor; use runtime sessions");
10400
+ const scanLogsQuery = scanLogs.command("query").description("Unavailable legacy query; use runtime sessions list").requiredOption("--interval <n>", "Time interval").requiredOption("--unit <unit>", "Time unit (hours)").option("--filter <filter>", "Filter: all, benign, threat", "all").option("--limit <n>", "Max results per page (API page size)", "50").option("--offset <n>", "Starting offset \u2014 rounds down to a page boundary", "0").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml");
10144
10401
  registerPageAliases(scanLogsQuery, { sizeFlag: "--page-size", sizeKey: "pageSize" });
10145
- scanLogsQuery.action(async (opts) => {
10146
- try {
10147
- const { page, size } = resolvePageParams(scanLogsQuery, opts, { indexBase: 1 });
10148
- const fmt = await resolveOutput(scanLogsQuery, opts);
10149
- if (fmt === "pretty") renderRuntimeConfigHeader();
10150
- const service = await createMgmtService();
10151
- const result = await service.queryScanLogs({
10152
- timeInterval: Number.parseInt(opts.interval, 10),
10153
- timeUnit: opts.unit,
10154
- pageNumber: page ?? 1,
10155
- pageSize: size ?? 50,
10156
- filter: opts.filter
10157
- });
10158
- renderScanLogList(result.results, result.pageToken, fmt);
10159
- } catch (err) {
10160
- fail(err);
10161
- }
10402
+ scanLogsQuery.action(() => {
10403
+ ui.error(
10404
+ "Legacy scan-logs retrieval is broken and under refactor. Empty results do not mean no activity. Use airs runtime sessions list --interval 1 --unit day --output json, or airs runtime report."
10405
+ );
10406
+ process.exitCode = 1;
10162
10407
  });
10163
10408
  const topics = runtime.command("topics").description("Manage AIRS custom topics and guardrail generation");
10164
10409
  registerApplyCommand(topics);
@@ -10306,7 +10551,12 @@ function buildProgram() {
10306
10551
  program.hook("preAction", async (_thisCommand, actionCommand) => {
10307
10552
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
10308
10553
  setQuiet(Boolean(root.quiet));
10309
- if (READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
10554
+ const isRuntimeReport = actionCommand.name() === "report" && actionCommand.parent?.name() === "runtime";
10555
+ if (isRuntimeReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
10556
+ usageError(
10557
+ "Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
10558
+ );
10559
+ if (!isRuntimeReport && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
10310
10560
  try {
10311
10561
  const format = await resolveOutput(actionCommand, actionCommand.opts());
10312
10562
  actionCommand.setOptionValueWithSource("output", format, "implied");
@@ -10315,8 +10565,18 @@ function buildProgram() {
10315
10565
  }
10316
10566
  }
10317
10567
  if (root.debug) {
10318
- const logPath = join4(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
10319
- installDebugLogger(logPath);
10568
+ const logPath = join4(
10569
+ process.cwd(),
10570
+ `debug-api-${Date.now()}-${randomUUID6().slice(0, 8)}.jsonl`
10571
+ );
10572
+ try {
10573
+ installDebugLogger(logPath);
10574
+ } catch {
10575
+ ui.error(
10576
+ "Cannot create the debug log in the current working directory. Run from a writable directory or omit --debug."
10577
+ );
10578
+ process.exit(1);
10579
+ }
10320
10580
  ui.status(`Debug: API log \u2192 ${logPath}`);
10321
10581
  }
10322
10582
  });