@cdot65/prisma-airs-cli 5.0.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/{chunk-IRLIFMGQ.js → chunk-7Z76VCB3.js} +797 -61
- package/dist/cli/index.js +278 -66
- package/dist/index.d.ts +87 -5
- package/dist/index.js +13 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
SdkPromptSetService,
|
|
10
10
|
SdkRedTeamService,
|
|
11
11
|
SdkRuntimeService,
|
|
12
|
+
collectGatewayEnvironmentReport,
|
|
13
|
+
collectRedTeamEnvironmentReport,
|
|
12
14
|
collectRuntimeDailyReport,
|
|
13
15
|
computeMetrics,
|
|
14
16
|
getOrCreateManagementClient,
|
|
@@ -16,6 +18,10 @@ import {
|
|
|
16
18
|
loadConfig,
|
|
17
19
|
readBackupDir,
|
|
18
20
|
readBackupFile,
|
|
21
|
+
renderEnvironmentReportHtml,
|
|
22
|
+
renderEnvironmentReportMarkdown,
|
|
23
|
+
renderRedTeamReportHtml,
|
|
24
|
+
renderRedTeamReportMarkdown,
|
|
19
25
|
renderRuntimeReportHtml,
|
|
20
26
|
renderRuntimeReportMarkdown,
|
|
21
27
|
resolveConfigFilePath,
|
|
@@ -24,7 +30,7 @@ import {
|
|
|
24
30
|
validateTopic,
|
|
25
31
|
writeBackupFile,
|
|
26
32
|
writeReportFile
|
|
27
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-7Z76VCB3.js";
|
|
28
34
|
|
|
29
35
|
// src/cli/index.ts
|
|
30
36
|
import "dotenv/config";
|
|
@@ -43,7 +49,7 @@ function installProcessGuards() {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
// src/cli/program.ts
|
|
46
|
-
import { randomUUID as
|
|
52
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
47
53
|
import { readFileSync as readFileSync4 } from "fs";
|
|
48
54
|
import { dirname as dirname4, join as join4 } from "path";
|
|
49
55
|
import { fileURLToPath } from "url";
|
|
@@ -3052,8 +3058,8 @@ function registerAiGatewayInference(parent) {
|
|
|
3052
3058
|
process.once("SIGINT", interrupt);
|
|
3053
3059
|
process.once("SIGTERM", interrupt);
|
|
3054
3060
|
process.stdout.on("error", outputError);
|
|
3055
|
-
const write = (text2) => new Promise((
|
|
3056
|
-
process.stdout.write(text2, (error) => error ? reject(error) :
|
|
3061
|
+
const write = (text2) => new Promise((resolve6, reject) => {
|
|
3062
|
+
process.stdout.write(text2, (error) => error ? reject(error) : resolve6());
|
|
3057
3063
|
});
|
|
3058
3064
|
const requestOptions = { signal: controller.signal };
|
|
3059
3065
|
const result = kind === "chat" ? await client.createChatCompletion(
|
|
@@ -4338,6 +4344,20 @@ function registerOrganisations(root) {
|
|
|
4338
4344
|
const self = showHelpOnEmpty(
|
|
4339
4345
|
group.command("self").description("Manage the current organisation")
|
|
4340
4346
|
);
|
|
4347
|
+
const info = addReadOutput(
|
|
4348
|
+
group.command("info").description("Get organisation dashboard info (settings redacted)").requiredOption("--tsg-id <id>", "Tenant service group ID")
|
|
4349
|
+
);
|
|
4350
|
+
info.action(
|
|
4351
|
+
(opts) => runDetail(
|
|
4352
|
+
info,
|
|
4353
|
+
opts,
|
|
4354
|
+
async (client) => redactAIGatewaySecrets(
|
|
4355
|
+
"organisations.getInfo",
|
|
4356
|
+
await client.organisations.getInfo(opts.tsgId),
|
|
4357
|
+
"response"
|
|
4358
|
+
)
|
|
4359
|
+
)
|
|
4360
|
+
);
|
|
4341
4361
|
const selfGet = addReadOutput(self.command("get").description("Get the current organisation"));
|
|
4342
4362
|
selfGet.action((opts) => runDetail(selfGet, opts, (client) => client.organisations.getSelf()));
|
|
4343
4363
|
const selfUpdate = addWriteOutput(
|
|
@@ -4428,6 +4448,10 @@ function registerAiGatewayInventory(root) {
|
|
|
4428
4448
|
{ option: "name", path: "name" },
|
|
4429
4449
|
{ option: "workspace", path: "workspace_id" }
|
|
4430
4450
|
];
|
|
4451
|
+
const catalog = addReadOutput(
|
|
4452
|
+
guardrails.command("catalog").description("Get available guardrail evaluator schemas (not enabled policies)")
|
|
4453
|
+
);
|
|
4454
|
+
catalog.action((opts) => runDetail(catalog, opts, (client) => client.guardrails.getCatalog()));
|
|
4431
4455
|
const addGuardrailFields = (command) => command.option("--actions <json>", "Guardrail actions object").option("--checks <json>", "Guardrail checks array").option("--name <name>", "Guardrail name").option("--workspace <uuid>", "Workspace UUID");
|
|
4432
4456
|
const guardrailUpdateFields = guardrailFields.filter((field) => field.option !== "workspace");
|
|
4433
4457
|
const addGuardrailUpdateFields = (command) => command.option("--actions <json>", "Guardrail actions object").option("--checks <json>", "Guardrail checks array").option("--name <name>", "Guardrail name");
|
|
@@ -4498,7 +4522,8 @@ function registerAiGatewayInventory(root) {
|
|
|
4498
4522
|
// src/cli/commands/aigateway/telemetry.ts
|
|
4499
4523
|
import {
|
|
4500
4524
|
AI_GW_GROUP_COLUMNS,
|
|
4501
|
-
AI_GW_GROUP_DIMENSIONS
|
|
4525
|
+
AI_GW_GROUP_DIMENSIONS,
|
|
4526
|
+
redactAIGatewaySecrets as redactAIGatewaySecrets2
|
|
4502
4527
|
} from "@cdot65/prisma-airs-sdk";
|
|
4503
4528
|
|
|
4504
4529
|
// src/cli/commands/aigateway/telemetry-filters.ts
|
|
@@ -4583,7 +4608,10 @@ function parseNamedDate2(value, flag) {
|
|
|
4583
4608
|
function registerMetric(telemetry, name, description, method) {
|
|
4584
4609
|
const command = addWindowOptions(telemetry.command(name).description(description));
|
|
4585
4610
|
command.action(
|
|
4586
|
-
(opts) => runDetail(command, opts, (client) =>
|
|
4611
|
+
(opts) => runDetail(command, opts, async (client) => {
|
|
4612
|
+
const result = await client.telemetry[method](windowFrom(opts));
|
|
4613
|
+
return method === "filterBoundaries" ? redactAIGatewaySecrets2("telemetry.filterBoundaries", result, "response") : result;
|
|
4614
|
+
})
|
|
4587
4615
|
);
|
|
4588
4616
|
}
|
|
4589
4617
|
function registerFilteredMetric(telemetry, name, description) {
|
|
@@ -4631,6 +4659,24 @@ function registerAiGatewayTelemetryReads(telemetry) {
|
|
|
4631
4659
|
);
|
|
4632
4660
|
registerMetric(telemetry, "error-trends", "Get error trends", "errorTrends");
|
|
4633
4661
|
registerMetric(telemetry, "errors", "Get error count", "errors");
|
|
4662
|
+
registerMetric(
|
|
4663
|
+
telemetry,
|
|
4664
|
+
"error-category-trends",
|
|
4665
|
+
"Get errors by HTTP status",
|
|
4666
|
+
"errorCategoryTrends"
|
|
4667
|
+
);
|
|
4668
|
+
registerMetric(
|
|
4669
|
+
telemetry,
|
|
4670
|
+
"grouped-errors",
|
|
4671
|
+
"Get HTTP error counts by time bucket",
|
|
4672
|
+
"groupedErrors"
|
|
4673
|
+
);
|
|
4674
|
+
registerMetric(
|
|
4675
|
+
telemetry,
|
|
4676
|
+
"filter-boundaries",
|
|
4677
|
+
"Get available analytics filters and bounds",
|
|
4678
|
+
"filterBoundaries"
|
|
4679
|
+
);
|
|
4634
4680
|
const feedback = showHelpOnEmpty(
|
|
4635
4681
|
telemetry.command("feedback").description("Inspect model feedback telemetry")
|
|
4636
4682
|
);
|
|
@@ -4673,7 +4719,7 @@ function registerAiGatewayTelemetryReads(telemetry) {
|
|
|
4673
4719
|
registerFilteredMetric(telemetry, "latency", "Get latency telemetry");
|
|
4674
4720
|
const logs = showHelpOnEmpty(telemetry.command("logs").description("Inspect request logs"));
|
|
4675
4721
|
const logsList = addWindowOptions(
|
|
4676
|
-
logs.command("list").description("List request logs").option("--page-size <n>", "Rows per response", "50").option("--status-code <code>", "Filter by HTTP status").option("--trace-id <id>", "Return one trace id")
|
|
4722
|
+
logs.command("list").description("List request logs").option("--page-size <n>", "Rows per response", "50").option("--current-page <n>", "Zero-based transaction page", "0").option("--status-code <code>", "Filter by HTTP status").option("--trace-id <id>", "Return one trace id")
|
|
4677
4723
|
);
|
|
4678
4724
|
logsList.action(
|
|
4679
4725
|
(opts) => runDetail(
|
|
@@ -4682,6 +4728,7 @@ function registerAiGatewayTelemetryReads(telemetry) {
|
|
|
4682
4728
|
(client) => client.telemetry.logs({
|
|
4683
4729
|
...windowFrom(opts),
|
|
4684
4730
|
pageSize: parsePositiveInteger2(opts.pageSize ?? "50", "--page-size"),
|
|
4731
|
+
currentPage: parseNonnegativePage(opts.currentPage ?? "0"),
|
|
4685
4732
|
...opts.statusCode ? { statusCode: parsePositiveInteger2(opts.statusCode, "--status-code") } : {},
|
|
4686
4733
|
...opts.traceId ? { traceId: opts.traceId } : {}
|
|
4687
4734
|
})
|
|
@@ -4693,6 +4740,99 @@ function registerAiGatewayTelemetryReads(telemetry) {
|
|
|
4693
4740
|
registerMetric(telemetry, "user-trends", "Get user trends", "userTrends");
|
|
4694
4741
|
registerMetric(telemetry, "users", "Get unique-user count", "users");
|
|
4695
4742
|
}
|
|
4743
|
+
function parseNonnegativePage(value) {
|
|
4744
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
4745
|
+
throw new CliUsageError("--current-page must be a nonnegative safe integer");
|
|
4746
|
+
return Number(value);
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
// src/cli/commands/aigateway-dashboard.ts
|
|
4750
|
+
import { lstat } from "fs/promises";
|
|
4751
|
+
import { resolve } from "path";
|
|
4752
|
+
import { AIGatewayClient as AIGatewayClient3 } from "@cdot65/prisma-airs-sdk";
|
|
4753
|
+
import { z } from "zod";
|
|
4754
|
+
import { randomUUID } from "crypto";
|
|
4755
|
+
function registerAiGatewayDashboardCommand(aigateway) {
|
|
4756
|
+
const command = aigateway.command("dashboard").description("Generate an AI Gateway environment dashboard (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
|
+
"--output-file <path>",
|
|
4758
|
+
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
4759
|
+
).option("--title <text>", "Report title", "AI Gateway environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (50 transactions/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
4760
|
+
"after",
|
|
4761
|
+
examples(
|
|
4762
|
+
"airs aigateway dashboard --workspace ws-develo-71f8d8 --output-file ./airs-daily.html",
|
|
4763
|
+
"airs aigateway dashboard --workspace ws-develo-71f8d8 --output markdown --output-file ./airs-daily.md",
|
|
4764
|
+
"airs aigateway dashboard --workspace ws-develo-71f8d8",
|
|
4765
|
+
"airs aigateway dashboard --workspace ws-develo-71f8d8 --strict --max-pages 20 --output-file - > daily.html"
|
|
4766
|
+
)
|
|
4767
|
+
).action(async (opts) => {
|
|
4768
|
+
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
4769
|
+
if (format !== "html" && format !== "markdown")
|
|
4770
|
+
usageError("Report output must be html or markdown");
|
|
4771
|
+
if (typeof opts.workspace !== "string" || !opts.workspace.trim())
|
|
4772
|
+
usageError("--workspace cannot be empty");
|
|
4773
|
+
if (!/^[1-9]\d*$/.test(opts.days) || !Number.isSafeInteger(Number(opts.days)))
|
|
4774
|
+
usageError("--days must be a positive integer");
|
|
4775
|
+
if (Boolean(opts.start) !== Boolean(opts.end))
|
|
4776
|
+
usageError("--start and --end must be supplied together");
|
|
4777
|
+
if (opts.start && command.getOptionValueSource("days") !== "default")
|
|
4778
|
+
usageError("Use --days or --start/--end, not both");
|
|
4779
|
+
const end = opts.end ? new Date(opts.end) : /* @__PURE__ */ new Date();
|
|
4780
|
+
const start = opts.start ? new Date(opts.start) : new Date(end.getTime() - Number(opts.days) * 864e5);
|
|
4781
|
+
const iso = z.string().datetime({ offset: true });
|
|
4782
|
+
if (opts.start && (!iso.safeParse(opts.start).success || !iso.safeParse(opts.end).success) || !Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start >= end)
|
|
4783
|
+
usageError("Provide a valid increasing ISO-8601 window with timezone");
|
|
4784
|
+
const maxPages = Number(opts.maxPages);
|
|
4785
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
|
|
4786
|
+
usageError("--max-pages must be an integer from 1 to 100");
|
|
4787
|
+
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
4788
|
+
usageError("--title must contain 1\u2013240 characters");
|
|
4789
|
+
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
4790
|
+
usageError("--output-file cannot be empty");
|
|
4791
|
+
const defaultName = `airs-aigateway-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
4792
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve(opts.outputFile ?? defaultName);
|
|
4793
|
+
try {
|
|
4794
|
+
if (outputPath) {
|
|
4795
|
+
const existing = await lstat(outputPath).catch((error) => {
|
|
4796
|
+
if (error.code !== "ENOENT") throw error;
|
|
4797
|
+
return void 0;
|
|
4798
|
+
});
|
|
4799
|
+
if (existing) throw new Error("Output already exists");
|
|
4800
|
+
}
|
|
4801
|
+
const config = await loadConfig();
|
|
4802
|
+
const client = new AIGatewayClient3({
|
|
4803
|
+
...aiGatewayClientOptions(config),
|
|
4804
|
+
numRetries: 0
|
|
4805
|
+
});
|
|
4806
|
+
ui.status("Collecting AI Gateway statistics and current configuration (read-only)...");
|
|
4807
|
+
const report = await collectGatewayEnvironmentReport(client, {
|
|
4808
|
+
title: opts.title,
|
|
4809
|
+
maxPages,
|
|
4810
|
+
workspace: opts.workspace,
|
|
4811
|
+
tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
|
|
4812
|
+
start,
|
|
4813
|
+
end
|
|
4814
|
+
});
|
|
4815
|
+
const rendered = format === "html" ? renderEnvironmentReportHtml(report) : renderEnvironmentReportMarkdown(report);
|
|
4816
|
+
if (outputPath) {
|
|
4817
|
+
await writeReportFile(outputPath, rendered);
|
|
4818
|
+
ui.status(`Report \u2192 ${outputPath.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
|
|
4819
|
+
} else {
|
|
4820
|
+
await new Promise(
|
|
4821
|
+
(done, reject) => process.stdout.write(rendered, (error) => error ? reject(error) : done())
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
const incomplete = report.sources.filter((source) => source.status !== "complete").length;
|
|
4825
|
+
if (incomplete)
|
|
4826
|
+
ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
|
|
4827
|
+
if (report.health === "unknown" || opts.strict && incomplete > 0) process.exitCode = 1;
|
|
4828
|
+
} catch {
|
|
4829
|
+
ui.error(
|
|
4830
|
+
"Report generation failed. Verify Management API configuration and a writable, new output path. Existing files are never overwritten."
|
|
4831
|
+
);
|
|
4832
|
+
process.exitCode = 1;
|
|
4833
|
+
}
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4696
4836
|
|
|
4697
4837
|
// src/cli/commands/aigateway.ts
|
|
4698
4838
|
async function createService() {
|
|
@@ -4756,6 +4896,7 @@ function scopeNameLooksUnrelated(name, scopeName) {
|
|
|
4756
4896
|
function registerAiGatewayCommand(program) {
|
|
4757
4897
|
const aigateway = program.command("aigateway").description("Manage and observe Prisma AIRS AI Gateway resources").action(() => aigateway.outputHelp());
|
|
4758
4898
|
registerAiGatewayInventory(aigateway);
|
|
4899
|
+
registerAiGatewayDashboardCommand(aigateway);
|
|
4759
4900
|
registerAiGatewayInference(aigateway);
|
|
4760
4901
|
const workspace = aigateway.command("workspaces").alias("workspace").description("Manage gateway workspaces").action(() => workspace.outputHelp());
|
|
4761
4902
|
const workspaceList = workspace.command("list").description("List workspaces (default: active workspaces you are scoped to)").option("--plane <plane>", "Plane to read from: data (scoped) or admin (whole tenant)").option("--status <status>", "Filter by lifecycle state: active or archived").option("--all", "Merge active + archived admin-plane reads (whole tenant, both states)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
|
|
@@ -5198,7 +5339,7 @@ function registerConfigCommand(program) {
|
|
|
5198
5339
|
}
|
|
5199
5340
|
|
|
5200
5341
|
// src/cli/commands/doctor.ts
|
|
5201
|
-
import { randomUUID } from "crypto";
|
|
5342
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5202
5343
|
import { readFile as readFile4 } from "fs/promises";
|
|
5203
5344
|
import { init, Scanner } from "@cdot65/prisma-airs-sdk";
|
|
5204
5345
|
var DOCTOR_TIMEOUT_MS = 5e3;
|
|
@@ -5296,8 +5437,8 @@ async function withTimeout(promise, ms) {
|
|
|
5296
5437
|
try {
|
|
5297
5438
|
return await Promise.race([
|
|
5298
5439
|
promise,
|
|
5299
|
-
new Promise((
|
|
5300
|
-
timer = setTimeout(() =>
|
|
5440
|
+
new Promise((resolve6) => {
|
|
5441
|
+
timer = setTimeout(() => resolve6(TIMED_OUT), ms);
|
|
5301
5442
|
})
|
|
5302
5443
|
]);
|
|
5303
5444
|
} finally {
|
|
@@ -5452,7 +5593,7 @@ async function defaultScannerProbe() {
|
|
|
5452
5593
|
const config = await loadConfig();
|
|
5453
5594
|
init(runtimeInitOptions(config));
|
|
5454
5595
|
const scanner = new Scanner();
|
|
5455
|
-
return scanner.queryByScanIds([
|
|
5596
|
+
return scanner.queryByScanIds([randomUUID2()]);
|
|
5456
5597
|
}
|
|
5457
5598
|
async function defaultMgmtProbe() {
|
|
5458
5599
|
const config = await loadConfig();
|
|
@@ -5567,12 +5708,12 @@ function hasBin(bin) {
|
|
|
5567
5708
|
}
|
|
5568
5709
|
}
|
|
5569
5710
|
function run(bin, args, label) {
|
|
5570
|
-
return new Promise((
|
|
5711
|
+
return new Promise((resolve6, reject) => {
|
|
5571
5712
|
const child = spawn(bin, args, { stdio: "inherit" });
|
|
5572
5713
|
child.on("error", (err) => reject(new Error(`Failed to start ${bin}: ${err.message}`)));
|
|
5573
5714
|
child.on("close", (code) => {
|
|
5574
5715
|
if (code !== 0) reject(new Error(`${label} failed with exit code ${code}`));
|
|
5575
|
-
else
|
|
5716
|
+
else resolve6();
|
|
5576
5717
|
});
|
|
5577
5718
|
});
|
|
5578
5719
|
}
|
|
@@ -6150,6 +6291,76 @@ async function backupTargets(opts) {
|
|
|
6150
6291
|
return results;
|
|
6151
6292
|
}
|
|
6152
6293
|
|
|
6294
|
+
// src/cli/commands/redteam-dashboard.ts
|
|
6295
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
6296
|
+
import { resolve as resolve2 } from "path";
|
|
6297
|
+
import { RedTeamClient } from "@cdot65/prisma-airs-sdk";
|
|
6298
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6299
|
+
function registerRedTeamDashboardCommand(redteam) {
|
|
6300
|
+
const command = redteam.command("dashboard").description("Generate a Red Team environment dashboard (read-only)").option("--output <format>", "Deliverable format: html or markdown (default: html)").option(
|
|
6301
|
+
"--output-file <path>",
|
|
6302
|
+
"Destination (default: new timestamped file in CWD); - for stdout; never overwrite"
|
|
6303
|
+
).option("--title <text>", "Report title", "Red Team environment report").option("--max-pages <n>", "Page budget per source, 1\u2013100 (15 records/page)", "40").option("--strict", "Exit 1 after writing if any source is incomplete").addHelpText(
|
|
6304
|
+
"after",
|
|
6305
|
+
examples(
|
|
6306
|
+
"airs redteam dashboard --output-file ./airs-daily.html",
|
|
6307
|
+
"airs redteam dashboard --output markdown --output-file ./airs-daily.md",
|
|
6308
|
+
"airs redteam dashboard",
|
|
6309
|
+
"airs redteam dashboard --strict --max-pages 20 --output-file - > daily.html"
|
|
6310
|
+
)
|
|
6311
|
+
).action(async (opts) => {
|
|
6312
|
+
const format = opts.output ?? command.parent?.parent?.opts().output ?? "html";
|
|
6313
|
+
if (format !== "html" && format !== "markdown")
|
|
6314
|
+
usageError("Report output must be html or markdown");
|
|
6315
|
+
const maxPages = Number(opts.maxPages);
|
|
6316
|
+
if (!/^[1-9]\d*$/.test(opts.maxPages) || !Number.isSafeInteger(maxPages) || maxPages > 100)
|
|
6317
|
+
usageError("--max-pages must be an integer from 1 to 100");
|
|
6318
|
+
if (typeof opts.title !== "string" || !opts.title.trim() || opts.title.length > 240)
|
|
6319
|
+
usageError("--title must contain 1\u2013240 characters");
|
|
6320
|
+
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
6321
|
+
usageError("--output-file cannot be empty");
|
|
6322
|
+
const defaultName = `airs-redteam-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID3().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
6323
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve2(opts.outputFile ?? defaultName);
|
|
6324
|
+
try {
|
|
6325
|
+
if (outputPath) {
|
|
6326
|
+
const existing = await lstat2(outputPath).catch((error) => {
|
|
6327
|
+
if (error.code !== "ENOENT") throw error;
|
|
6328
|
+
return void 0;
|
|
6329
|
+
});
|
|
6330
|
+
if (existing) throw new Error("Output already exists");
|
|
6331
|
+
}
|
|
6332
|
+
const config = await loadConfig();
|
|
6333
|
+
const client = new RedTeamClient({
|
|
6334
|
+
...redTeamClientOptions(config),
|
|
6335
|
+
numRetries: 0
|
|
6336
|
+
});
|
|
6337
|
+
ui.status("Collecting Red Team statistics and current configuration (read-only)...");
|
|
6338
|
+
const report = await collectRedTeamEnvironmentReport(client, {
|
|
6339
|
+
title: opts.title,
|
|
6340
|
+
maxPages
|
|
6341
|
+
});
|
|
6342
|
+
const rendered = format === "html" ? renderRedTeamReportHtml(report) : renderRedTeamReportMarkdown(report);
|
|
6343
|
+
if (outputPath) {
|
|
6344
|
+
await writeReportFile(outputPath, rendered);
|
|
6345
|
+
ui.status(`Report \u2192 ${outputPath.replace(/[\p{Cc}\p{Cf}]/gu, "_")} (mode 0600)`);
|
|
6346
|
+
} else {
|
|
6347
|
+
await new Promise(
|
|
6348
|
+
(done, reject) => process.stdout.write(rendered, (error) => error ? reject(error) : done())
|
|
6349
|
+
);
|
|
6350
|
+
}
|
|
6351
|
+
const incomplete = report.sources.filter((source) => source.status !== "complete").length;
|
|
6352
|
+
if (incomplete)
|
|
6353
|
+
ui.status(`${incomplete} source(s) incomplete; review the evidence section.`);
|
|
6354
|
+
if (report.health === "unknown" || opts.strict && incomplete > 0) process.exitCode = 1;
|
|
6355
|
+
} catch {
|
|
6356
|
+
ui.error(
|
|
6357
|
+
"Report generation failed. Verify Management API configuration and a writable, new output path. Existing files are never overwritten."
|
|
6358
|
+
);
|
|
6359
|
+
process.exitCode = 1;
|
|
6360
|
+
}
|
|
6361
|
+
});
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6153
6364
|
// src/cli/commands/restore.ts
|
|
6154
6365
|
var SERVER_DERIVED_FIELDS = [
|
|
6155
6366
|
"uuid",
|
|
@@ -6392,6 +6603,7 @@ function parseAdapterVariables(input) {
|
|
|
6392
6603
|
}
|
|
6393
6604
|
function registerRedteamCommand(program) {
|
|
6394
6605
|
const redteam = program.command("redteam").description("AI Red Team scan operations");
|
|
6606
|
+
registerRedTeamDashboardCommand(redteam);
|
|
6395
6607
|
redteam.command("abort <jobId>").description("Abort a running scan").action(async (jobId) => {
|
|
6396
6608
|
try {
|
|
6397
6609
|
renderRedteamHeader();
|
|
@@ -7385,7 +7597,7 @@ function registerRedteamCommand(program) {
|
|
|
7385
7597
|
}
|
|
7386
7598
|
|
|
7387
7599
|
// src/cli/commands/runtime.ts
|
|
7388
|
-
import { randomUUID as
|
|
7600
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
7389
7601
|
import * as fs5 from "fs";
|
|
7390
7602
|
import { readFile as readFile10 } from "fs/promises";
|
|
7391
7603
|
import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
|
|
@@ -7633,7 +7845,7 @@ function mergeProfilePolicy(existing, overrides) {
|
|
|
7633
7845
|
}
|
|
7634
7846
|
|
|
7635
7847
|
// src/cli/bulk-scan-lock.ts
|
|
7636
|
-
import { randomUUID as
|
|
7848
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7637
7849
|
import * as fs3 from "fs/promises";
|
|
7638
7850
|
function errorCode(error) {
|
|
7639
7851
|
return error?.code;
|
|
@@ -7664,7 +7876,7 @@ function processIsAlive(pid) {
|
|
|
7664
7876
|
}
|
|
7665
7877
|
}
|
|
7666
7878
|
async function installLock(lockPath, record) {
|
|
7667
|
-
const candidate = `${lockPath}.candidate-${process.pid}-${
|
|
7879
|
+
const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID4()}`;
|
|
7668
7880
|
try {
|
|
7669
7881
|
await fs3.writeFile(candidate, JSON.stringify(record), {
|
|
7670
7882
|
encoding: "utf-8",
|
|
@@ -7682,7 +7894,7 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7682
7894
|
version: 1,
|
|
7683
7895
|
pid: process.pid,
|
|
7684
7896
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7685
|
-
token:
|
|
7897
|
+
token: randomUUID4()
|
|
7686
7898
|
};
|
|
7687
7899
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7688
7900
|
try {
|
|
@@ -7718,32 +7930,32 @@ async function acquireBulkScanLock(statePath) {
|
|
|
7718
7930
|
}
|
|
7719
7931
|
|
|
7720
7932
|
// src/cli/bulk-scan-state.ts
|
|
7721
|
-
import { randomUUID as
|
|
7933
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7722
7934
|
import * as fs4 from "fs/promises";
|
|
7723
7935
|
import * as path2 from "path";
|
|
7724
|
-
import { z } from "zod";
|
|
7725
|
-
var BulkScanResultSchema =
|
|
7726
|
-
index:
|
|
7727
|
-
reqId:
|
|
7728
|
-
prompt:
|
|
7729
|
-
response:
|
|
7730
|
-
scanId:
|
|
7731
|
-
reportId:
|
|
7732
|
-
action:
|
|
7733
|
-
category:
|
|
7734
|
-
triggered:
|
|
7735
|
-
detections:
|
|
7736
|
-
error:
|
|
7936
|
+
import { z as z2 } from "zod";
|
|
7937
|
+
var BulkScanResultSchema = z2.object({
|
|
7938
|
+
index: z2.number().int().nonnegative(),
|
|
7939
|
+
reqId: z2.number().int().nonnegative(),
|
|
7940
|
+
prompt: z2.string(),
|
|
7941
|
+
response: z2.string().optional(),
|
|
7942
|
+
scanId: z2.string(),
|
|
7943
|
+
reportId: z2.string(),
|
|
7944
|
+
action: z2.enum(["allow", "block", "failed"]),
|
|
7945
|
+
category: z2.string(),
|
|
7946
|
+
triggered: z2.boolean(),
|
|
7947
|
+
detections: z2.record(z2.boolean()),
|
|
7948
|
+
error: z2.string().optional()
|
|
7737
7949
|
});
|
|
7738
|
-
var BulkScanItemSchema =
|
|
7739
|
-
index:
|
|
7740
|
-
reqId:
|
|
7741
|
-
prompt:
|
|
7742
|
-
status:
|
|
7743
|
-
scanId:
|
|
7744
|
-
receiptReportId:
|
|
7950
|
+
var BulkScanItemSchema = z2.object({
|
|
7951
|
+
index: z2.number().int().nonnegative(),
|
|
7952
|
+
reqId: z2.number().int().nonnegative(),
|
|
7953
|
+
prompt: z2.string(),
|
|
7954
|
+
status: z2.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
|
|
7955
|
+
scanId: z2.string().min(1).optional(),
|
|
7956
|
+
receiptReportId: z2.string().optional(),
|
|
7745
7957
|
result: BulkScanResultSchema.optional(),
|
|
7746
|
-
error:
|
|
7958
|
+
error: z2.string().optional()
|
|
7747
7959
|
}).superRefine((item, ctx) => {
|
|
7748
7960
|
if (item.reqId !== item.index) {
|
|
7749
7961
|
ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
|
|
@@ -7781,15 +7993,15 @@ var BulkScanItemSchema = z.object({
|
|
|
7781
7993
|
}
|
|
7782
7994
|
}
|
|
7783
7995
|
});
|
|
7784
|
-
var BulkScanStateSchema =
|
|
7785
|
-
version:
|
|
7786
|
-
profile:
|
|
7787
|
-
sessionId:
|
|
7788
|
-
outputFile:
|
|
7789
|
-
batchSize:
|
|
7790
|
-
createdAt:
|
|
7791
|
-
updatedAt:
|
|
7792
|
-
items:
|
|
7996
|
+
var BulkScanStateSchema = z2.object({
|
|
7997
|
+
version: z2.literal(2),
|
|
7998
|
+
profile: z2.string().min(1),
|
|
7999
|
+
sessionId: z2.string().optional(),
|
|
8000
|
+
outputFile: z2.string().min(1),
|
|
8001
|
+
batchSize: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
8002
|
+
createdAt: z2.string().datetime(),
|
|
8003
|
+
updatedAt: z2.string().datetime(),
|
|
8004
|
+
items: z2.array(BulkScanItemSchema).min(1)
|
|
7793
8005
|
}).superRefine((state, ctx) => {
|
|
7794
8006
|
const indices = /* @__PURE__ */ new Set();
|
|
7795
8007
|
for (const [position, item] of state.items.entries()) {
|
|
@@ -7815,8 +8027,8 @@ async function saveBulkScanState(state, dir, filePath) {
|
|
|
7815
8027
|
}
|
|
7816
8028
|
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
7817
8029
|
if (!filePath) await fs4.chmod(dir, 448);
|
|
7818
|
-
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${
|
|
7819
|
-
const temporary = `${target}.tmp-${process.pid}-${
|
|
8030
|
+
const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID5()}.bulk-scan.json`);
|
|
8031
|
+
const temporary = `${target}.tmp-${process.pid}-${randomUUID5()}`;
|
|
7820
8032
|
try {
|
|
7821
8033
|
await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
|
|
7822
8034
|
encoding: "utf-8",
|
|
@@ -8941,8 +9153,8 @@ function registerCleanupCommand(parent) {
|
|
|
8941
9153
|
}
|
|
8942
9154
|
|
|
8943
9155
|
// src/cli/commands/runtime-dashboard.ts
|
|
8944
|
-
import { lstat } from "fs/promises";
|
|
8945
|
-
import { resolve as
|
|
9156
|
+
import { lstat as lstat3 } from "fs/promises";
|
|
9157
|
+
import { resolve as resolve4 } from "path";
|
|
8946
9158
|
import { dump as dump5 } from "js-yaml";
|
|
8947
9159
|
var TIME_UNITS = ["hour", "hours", "day", "days"];
|
|
8948
9160
|
function integer2(value, name, minimum = 0) {
|
|
@@ -9018,8 +9230,8 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9018
9230
|
"Scan content requires exactly one of --show-content or --output-file <new-path>"
|
|
9019
9231
|
);
|
|
9020
9232
|
if (opts.outputFile) {
|
|
9021
|
-
const path3 =
|
|
9022
|
-
const existing = await
|
|
9233
|
+
const path3 = resolve4(text(opts.outputFile, "--output-file"));
|
|
9234
|
+
const existing = await lstat3(path3).catch((error) => {
|
|
9023
9235
|
if (error.code !== "ENOENT") throw error;
|
|
9024
9236
|
return void 0;
|
|
9025
9237
|
});
|
|
@@ -9034,7 +9246,7 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9034
9246
|
const result = await read(client.dashboard);
|
|
9035
9247
|
const output = format === "yaml" ? dump5(result, { noRefs: true, lineWidth: -1 }).trimEnd() : JSON.stringify(result, null, 2);
|
|
9036
9248
|
if (command.name() === "scan-content" && opts.outputFile) {
|
|
9037
|
-
await writeReportFile(
|
|
9249
|
+
await writeReportFile(resolve4(String(opts.outputFile)), `${output}
|
|
9038
9250
|
`);
|
|
9039
9251
|
ui.status("Scan content exported to the requested private file (0600).");
|
|
9040
9252
|
} else console.log(output);
|
|
@@ -9205,9 +9417,9 @@ function registerRuntimeDashboardCommands(runtime) {
|
|
|
9205
9417
|
}
|
|
9206
9418
|
|
|
9207
9419
|
// src/cli/commands/runtime-report.ts
|
|
9208
|
-
import { lstat as
|
|
9209
|
-
import { resolve as
|
|
9210
|
-
import { randomUUID as
|
|
9420
|
+
import { lstat as lstat4 } from "fs/promises";
|
|
9421
|
+
import { resolve as resolve5 } from "path";
|
|
9422
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9211
9423
|
function registerRuntimeReportCommand(runtime) {
|
|
9212
9424
|
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(
|
|
9213
9425
|
"--output-file <path>",
|
|
@@ -9231,11 +9443,11 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
9231
9443
|
usageError("--title must contain 1\u2013240 characters");
|
|
9232
9444
|
if (opts.outputFile !== void 0 && !opts.outputFile.trim())
|
|
9233
9445
|
usageError("--output-file cannot be empty");
|
|
9234
|
-
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${
|
|
9235
|
-
const outputPath = opts.outputFile === "-" ? void 0 :
|
|
9446
|
+
const defaultName = `airs-runtime-report-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID6().slice(0, 8)}.${format === "html" ? "html" : "md"}`;
|
|
9447
|
+
const outputPath = opts.outputFile === "-" ? void 0 : resolve5(opts.outputFile ?? defaultName);
|
|
9236
9448
|
try {
|
|
9237
9449
|
if (outputPath) {
|
|
9238
|
-
const existing = await
|
|
9450
|
+
const existing = await lstat4(outputPath).catch((error) => {
|
|
9239
9451
|
if (error.code !== "ENOENT") throw error;
|
|
9240
9452
|
return void 0;
|
|
9241
9453
|
});
|
|
@@ -9722,7 +9934,7 @@ function completedBulkResults(state) {
|
|
|
9722
9934
|
}
|
|
9723
9935
|
async function writeBulkResults(outputPath, results) {
|
|
9724
9936
|
await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
|
|
9725
|
-
const temporary = `${outputPath}.tmp-${process.pid}-${
|
|
9937
|
+
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID7()}`;
|
|
9726
9938
|
try {
|
|
9727
9939
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
9728
9940
|
encoding: "utf-8",
|
|
@@ -10567,12 +10779,12 @@ function buildProgram() {
|
|
|
10567
10779
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
10568
10780
|
const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
|
|
10569
10781
|
setQuiet(Boolean(root.quiet));
|
|
10570
|
-
const
|
|
10571
|
-
if (
|
|
10782
|
+
const isEnvironmentReport = actionCommand.name() === "report" && actionCommand.parent?.name() === "runtime" || actionCommand.name() === "dashboard" && ["redteam", "aigateway"].includes(actionCommand.parent?.name() ?? "");
|
|
10783
|
+
if (isEnvironmentReport && (root.debug || /^(1|true|yes|on)$/i.test(process.env.PANW_AI_SEC_DEBUG?.trim() ?? "")))
|
|
10572
10784
|
usageError(
|
|
10573
10785
|
"Disable --debug and PANW_AI_SEC_DEBUG for environment reports to avoid persisting sensitive traffic content"
|
|
10574
10786
|
);
|
|
10575
|
-
if (!
|
|
10787
|
+
if (!isEnvironmentReport && READ_COMMAND_NAMES.has(actionCommand.name()) && actionCommand.options.some((option) => option.long === "--output")) {
|
|
10576
10788
|
try {
|
|
10577
10789
|
const format = await resolveOutput(actionCommand, actionCommand.opts());
|
|
10578
10790
|
actionCommand.setOptionValueWithSource("output", format, "implied");
|
|
@@ -10583,7 +10795,7 @@ function buildProgram() {
|
|
|
10583
10795
|
if (root.debug) {
|
|
10584
10796
|
const logPath = join4(
|
|
10585
10797
|
process.cwd(),
|
|
10586
|
-
`debug-api-${Date.now()}-${
|
|
10798
|
+
`debug-api-${Date.now()}-${randomUUID8().slice(0, 8)}.jsonl`
|
|
10587
10799
|
);
|
|
10588
10800
|
try {
|
|
10589
10801
|
installDebugLogger(logPath);
|