@cdot65/prisma-airs-cli 3.0.1 → 3.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/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AirsScanService,
4
4
  ConfigSchema,
5
5
  RateLimitedScanService,
6
+ SDK_ASYNC_BATCH_SIZE,
6
7
  SdkManagementService,
7
8
  SdkModelSecurityService,
8
9
  SdkPromptSetService,
@@ -19,7 +20,7 @@ import {
19
20
  sanitizeFilename,
20
21
  validateTopic,
21
22
  writeBackupFile
22
- } from "../chunk-JXHYQFEK.js";
23
+ } from "../chunk-TTBN7YHC.js";
23
24
 
24
25
  // src/cli/index.ts
25
26
  import "dotenv/config";
@@ -40,7 +41,7 @@ function installProcessGuards() {
40
41
  // src/cli/program.ts
41
42
  import { readFileSync as readFileSync4 } from "fs";
42
43
  import { homedir } from "os";
43
- import { dirname as dirname3, join as join3 } from "path";
44
+ import { dirname as dirname4, join as join4 } from "path";
44
45
  import { fileURLToPath } from "url";
45
46
  import { Command } from "commander";
46
47
 
@@ -1032,6 +1033,171 @@ function renderLabelValues(key, values) {
1032
1033
  }
1033
1034
  console.log();
1034
1035
  }
1036
+ function renderModelList(models, format = "pretty") {
1037
+ if (models.length === 0) {
1038
+ ui.emptyList("models");
1039
+ return;
1040
+ }
1041
+ if (format !== "pretty") {
1042
+ const rows = models.map((m) => ({
1043
+ id: m.uuid,
1044
+ name: m.name,
1045
+ outcome: m.latestVersionOutcome ?? "",
1046
+ formats: (m.latestVersionFormats ?? []).join(", "),
1047
+ scanned: m.latestVersionScanTime ?? ""
1048
+ }));
1049
+ console.log(
1050
+ formatOutput(
1051
+ rows,
1052
+ [
1053
+ { key: "id", label: "ID" },
1054
+ { key: "name", label: "Name" },
1055
+ { key: "outcome", label: "Outcome" },
1056
+ { key: "formats", label: "Formats" },
1057
+ { key: "scanned", label: "Last Scan" }
1058
+ ],
1059
+ format
1060
+ )
1061
+ );
1062
+ return;
1063
+ }
1064
+ ui.section("Models:");
1065
+ for (const m of models) {
1066
+ ui.dim(m.uuid);
1067
+ const outcome = m.latestVersionOutcome ? stateColor(m.latestVersionOutcome)(m.latestVersionOutcome) : chalk6.dim("unscanned");
1068
+ const formats = m.latestVersionFormats && m.latestVersionFormats.length > 0 ? chalk6.dim(` [${m.latestVersionFormats.join(", ")}]`) : "";
1069
+ console.log(` ${m.name} ${outcome}${formats}`);
1070
+ console.log();
1071
+ }
1072
+ }
1073
+ function renderModelDetail(model, format = "pretty") {
1074
+ if (format !== "pretty") {
1075
+ console.log(format === "json" ? JSON.stringify(model, null, 2) : yamlDump2(model));
1076
+ return;
1077
+ }
1078
+ ui.section("Model Detail:");
1079
+ const pairs = [
1080
+ ["UUID", model.uuid],
1081
+ ["Name", model.name],
1082
+ ["Created", model.createdAt],
1083
+ ["Updated", model.updatedAt]
1084
+ ];
1085
+ if (model.latestVersionUuid != null) pairs.push(["Latest Version", model.latestVersionUuid]);
1086
+ if (model.latestVersionRevision != null)
1087
+ pairs.push(["Latest Revision", model.latestVersionRevision]);
1088
+ if (model.latestVersionOutcome != null)
1089
+ pairs.push([
1090
+ "Latest Outcome",
1091
+ stateColor(model.latestVersionOutcome)(model.latestVersionOutcome)
1092
+ ]);
1093
+ if (model.latestVersionFormats?.length)
1094
+ pairs.push(["Formats", model.latestVersionFormats.join(", ")]);
1095
+ if (model.latestVersionSourceTypes?.length)
1096
+ pairs.push(["Source Types", model.latestVersionSourceTypes.join(", ")]);
1097
+ if (model.latestVersionScanTime != null) pairs.push(["Last Scan", model.latestVersionScanTime]);
1098
+ ui.keyValue(pairs);
1099
+ console.log();
1100
+ }
1101
+ function renderModelVersionList(versions, format = "pretty") {
1102
+ if (versions.length === 0) {
1103
+ ui.emptyList("versions");
1104
+ return;
1105
+ }
1106
+ if (format !== "pretty") {
1107
+ const rows = versions.map((v) => ({
1108
+ id: v.uuid,
1109
+ revision: v.revision,
1110
+ files: v.fileCount ?? "",
1111
+ outcome: v.lastEvalOutcome ?? "",
1112
+ scanned: v.latestScanTime ?? ""
1113
+ }));
1114
+ console.log(
1115
+ formatOutput(
1116
+ rows,
1117
+ [
1118
+ { key: "id", label: "ID" },
1119
+ { key: "revision", label: "Revision" },
1120
+ { key: "files", label: "Files" },
1121
+ { key: "outcome", label: "Outcome" },
1122
+ { key: "scanned", label: "Last Scan" }
1123
+ ],
1124
+ format
1125
+ )
1126
+ );
1127
+ return;
1128
+ }
1129
+ ui.section("Model Versions:");
1130
+ for (const v of versions) {
1131
+ ui.dim(v.uuid);
1132
+ const outcome = v.lastEvalOutcome ? stateColor(v.lastEvalOutcome)(v.lastEvalOutcome) : chalk6.dim("unscanned");
1133
+ const files = v.fileCount != null ? ` files: ${v.fileCount}` : "";
1134
+ console.log(` ${v.revision} ${outcome}${files}`);
1135
+ console.log();
1136
+ }
1137
+ }
1138
+ function renderModelVersionDetail(version, format = "pretty") {
1139
+ if (format !== "pretty") {
1140
+ console.log(format === "json" ? JSON.stringify(version, null, 2) : yamlDump2(version));
1141
+ return;
1142
+ }
1143
+ ui.section("Model Version Detail:");
1144
+ const pairs = [
1145
+ ["UUID", version.uuid],
1146
+ ["Model", version.modelUuid],
1147
+ ["Revision", version.revision],
1148
+ ["Created", version.createdAt],
1149
+ ["Updated", version.updatedAt]
1150
+ ];
1151
+ if (version.fileCount != null) pairs.push(["File Count", version.fileCount]);
1152
+ if (version.license != null) pairs.push(["License", version.license]);
1153
+ if (version.modelFormats?.length) pairs.push(["Formats", version.modelFormats.join(", ")]);
1154
+ if (version.sourceTypes?.length) pairs.push(["Source Types", version.sourceTypes.join(", ")]);
1155
+ if (version.hfModelName != null) pairs.push(["HF Model", version.hfModelName]);
1156
+ if (version.hfOrganization != null) pairs.push(["HF Organization", version.hfOrganization]);
1157
+ if (version.lastEvalOutcome != null)
1158
+ pairs.push(["Last Outcome", stateColor(version.lastEvalOutcome)(version.lastEvalOutcome)]);
1159
+ if (version.latestScanTime != null) pairs.push(["Last Scan", version.latestScanTime]);
1160
+ ui.keyValue(pairs);
1161
+ if (version.lastEvalSummary) {
1162
+ ui.section("Last Eval Summary:");
1163
+ ui.keyValue([
1164
+ ["Passed", version.lastEvalSummary.rulesPassed],
1165
+ ["Failed", version.lastEvalSummary.rulesFailed],
1166
+ ["Total", version.lastEvalSummary.totalRules]
1167
+ ]);
1168
+ }
1169
+ console.log();
1170
+ }
1171
+ function renderModelFileList(files, format = "pretty") {
1172
+ if (files.length === 0) {
1173
+ ui.emptyList("files");
1174
+ return;
1175
+ }
1176
+ if (format !== "pretty") {
1177
+ const rows = files.map((f) => ({
1178
+ id: f.uuid,
1179
+ path: f.path,
1180
+ type: f.type,
1181
+ formats: f.formats.join(", "),
1182
+ result: f.result
1183
+ }));
1184
+ console.log(
1185
+ formatOutput(
1186
+ rows,
1187
+ [
1188
+ { key: "id", label: "ID" },
1189
+ { key: "path", label: "Path" },
1190
+ { key: "type", label: "Type" },
1191
+ { key: "formats", label: "Formats" },
1192
+ { key: "result", label: "Result" }
1193
+ ],
1194
+ format
1195
+ )
1196
+ );
1197
+ return;
1198
+ }
1199
+ renderFileList(files);
1200
+ }
1035
1201
 
1036
1202
  // src/cli/renderer/redteam.ts
1037
1203
  import chalk7 from "chalk";
@@ -1582,6 +1748,170 @@ function renderRegistryCredentials(creds, format = "pretty") {
1582
1748
  ]);
1583
1749
  console.log();
1584
1750
  }
1751
+ function channelStatusColor(status) {
1752
+ switch (status.toUpperCase()) {
1753
+ case "ONLINE":
1754
+ return chalk7.green;
1755
+ case "DRAFT":
1756
+ return chalk7.yellow;
1757
+ case "OFFLINE":
1758
+ return chalk7.red;
1759
+ default:
1760
+ return chalk7.white;
1761
+ }
1762
+ }
1763
+ function renderChannelList(channels, format = "pretty") {
1764
+ if (channels.length === 0) {
1765
+ ui.emptyList("channels");
1766
+ return;
1767
+ }
1768
+ if (format !== "pretty") {
1769
+ const rows = channels.map((c) => ({
1770
+ id: c.uuid ?? "",
1771
+ name: c.name ?? "",
1772
+ status: c.status ?? "",
1773
+ clients: c.connectedClientsCount ?? "",
1774
+ lastOnline: c.lastOnlineAt ?? ""
1775
+ }));
1776
+ console.log(
1777
+ formatOutput(
1778
+ rows,
1779
+ [
1780
+ { key: "id", label: "ID" },
1781
+ { key: "name", label: "Name" },
1782
+ { key: "status", label: "Status" },
1783
+ { key: "clients", label: "Clients" },
1784
+ { key: "lastOnline", label: "Last Online" }
1785
+ ],
1786
+ format
1787
+ )
1788
+ );
1789
+ return;
1790
+ }
1791
+ ui.section("Network Broker Channels:");
1792
+ for (const c of channels) {
1793
+ if (c.uuid) ui.dim(c.uuid);
1794
+ const status = c.status ? channelStatusColor(c.status)(c.status) : chalk7.dim("unknown");
1795
+ const clients = c.connectedClientsCount != null ? ` clients: ${c.connectedClientsCount}` : "";
1796
+ console.log(` ${c.name ?? "(unnamed)"} ${status}${clients}`);
1797
+ console.log();
1798
+ }
1799
+ }
1800
+ function renderChannelDetail(channel, format = "pretty") {
1801
+ if (format !== "pretty") {
1802
+ console.log(format === "json" ? JSON.stringify(channel, null, 2) : yamlDump3(channel));
1803
+ return;
1804
+ }
1805
+ ui.section("Channel Detail:");
1806
+ const pairs = [["UUID", channel.uuid]];
1807
+ if (channel.name != null) pairs.push(["Name", channel.name]);
1808
+ if (channel.description != null) pairs.push(["Description", channel.description]);
1809
+ if (channel.status != null)
1810
+ pairs.push(["Status", channelStatusColor(channel.status)(channel.status)]);
1811
+ if (channel.connectedClientsCount != null)
1812
+ pairs.push(["Connected Clients", channel.connectedClientsCount]);
1813
+ if (channel.outdatedClientsCount != null)
1814
+ pairs.push(["Outdated Clients", channel.outdatedClientsCount]);
1815
+ if (channel.lastOnlineAt != null) pairs.push(["Last Online", channel.lastOnlineAt]);
1816
+ if (channel.addedBy != null) pairs.push(["Added By", channel.addedBy]);
1817
+ if (channel.createdAt != null) pairs.push(["Created", channel.createdAt]);
1818
+ if (channel.updatedAt != null) pairs.push(["Updated", channel.updatedAt]);
1819
+ ui.keyValue(pairs);
1820
+ if (channel.features && Object.keys(channel.features).length > 0) {
1821
+ ui.section("Features:");
1822
+ ui.keyValue(Object.entries(channel.features).map(([k, v]) => [k, v]));
1823
+ }
1824
+ console.log();
1825
+ }
1826
+ function renderChannelStats(stats, format = "pretty") {
1827
+ if (format !== "pretty") {
1828
+ console.log(format === "json" ? JSON.stringify(stats, null, 2) : yamlDump3(stats));
1829
+ return;
1830
+ }
1831
+ ui.section("Network Broker Stats:");
1832
+ const pairs = [];
1833
+ if (stats.onlineChannels != null) pairs.push(["Online Channels", stats.onlineChannels]);
1834
+ if (stats.totalChannels != null) pairs.push(["Total Channels", stats.totalChannels]);
1835
+ if (stats.serverDomain != null) pairs.push(["Server Domain", stats.serverDomain]);
1836
+ if (stats.dockerRegistry != null) pairs.push(["Docker Registry", stats.dockerRegistry]);
1837
+ if (stats.dockerImage != null) pairs.push(["Docker Image", stats.dockerImage]);
1838
+ if (stats.helmChart != null) pairs.push(["Helm Chart", stats.helmChart]);
1839
+ if (stats.clientVersion != null) pairs.push(["Client Version", stats.clientVersion]);
1840
+ ui.keyValue(pairs);
1841
+ console.log();
1842
+ }
1843
+ function renderLanguages(data, format = "pretty") {
1844
+ if (format !== "pretty") {
1845
+ if (format === "json" || format === "yaml") {
1846
+ console.log(format === "json" ? JSON.stringify(data, null, 2) : yamlDump3(data));
1847
+ return;
1848
+ }
1849
+ console.log(
1850
+ formatOutput(
1851
+ data.languages.map((l) => ({ code: l.code, name: l.name })),
1852
+ [
1853
+ { key: "code", label: "Code" },
1854
+ { key: "name", label: "Name" }
1855
+ ],
1856
+ format
1857
+ )
1858
+ );
1859
+ return;
1860
+ }
1861
+ ui.section("Tenant Languages:");
1862
+ ui.keyValue([
1863
+ ["Multilingual", data.multilingualEnabled ? activeState(true) : activeState(false)],
1864
+ ["Supported Job Types", data.supportedJobTypes.join(", ") || "(none)"]
1865
+ ]);
1866
+ if (data.languages.length === 0) {
1867
+ ui.emptyList("languages");
1868
+ return;
1869
+ }
1870
+ ui.section("Languages:");
1871
+ for (const l of data.languages) {
1872
+ console.log(` ${chalk7.dim(l.code)} ${l.name}`);
1873
+ }
1874
+ console.log();
1875
+ }
1876
+ function renderErrorLogs(logs, format = "pretty") {
1877
+ if (logs.length === 0) {
1878
+ ui.emptyList("error logs");
1879
+ return;
1880
+ }
1881
+ if (format !== "pretty") {
1882
+ const rows = logs.map((l) => ({
1883
+ createdAt: l.createdAt,
1884
+ errorType: l.errorType ?? "",
1885
+ errorSource: l.errorSource ?? "",
1886
+ jobId: l.jobId ?? "",
1887
+ message: l.errorMessage ?? ""
1888
+ }));
1889
+ console.log(
1890
+ formatOutput(
1891
+ rows,
1892
+ [
1893
+ { key: "createdAt", label: "Created" },
1894
+ { key: "errorType", label: "Type" },
1895
+ { key: "errorSource", label: "Source" },
1896
+ { key: "jobId", label: "Job" },
1897
+ { key: "message", label: "Message" }
1898
+ ],
1899
+ format
1900
+ )
1901
+ );
1902
+ return;
1903
+ }
1904
+ ui.section("Target-Profile Error Logs:");
1905
+ for (const l of logs) {
1906
+ const type = l.errorType ? chalk7.red(l.errorType) : chalk7.dim("error");
1907
+ console.log(
1908
+ ` ${chalk7.dim(l.createdAt)} ${type}${l.errorSource ? ` (${l.errorSource})` : ""}`
1909
+ );
1910
+ if (l.errorMessage) console.log(` ${l.errorMessage}`);
1911
+ if (l.jobId) console.log(` ${chalk7.dim(`job: ${l.jobId}`)}`);
1912
+ console.log();
1913
+ }
1914
+ }
1585
1915
 
1586
1916
  // src/cli/renderer/runtime.ts
1587
1917
  import chalk8 from "chalk";
@@ -2340,7 +2670,8 @@ function redTeamClientOptions(config) {
2340
2670
  tsgId: config.mgmtTsgId,
2341
2671
  dataEndpoint: config.redTeamDataEndpoint,
2342
2672
  mgmtEndpoint: config.redTeamMgmtEndpoint,
2343
- tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint
2673
+ tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint,
2674
+ networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint
2344
2675
  };
2345
2676
  }
2346
2677
  function modelSecurityClientOptions(config) {
@@ -3057,6 +3388,76 @@ function registerModelSecurityCommand(program) {
3057
3388
  fail(err);
3058
3389
  }
3059
3390
  });
3391
+ const models = ms.command("models").description("Browse the scanned model catalog (read-only)");
3392
+ 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, csv, json, yaml", "pretty").addHelpText("after", examples("airs model-security models list")).action(async (opts) => {
3393
+ try {
3394
+ const fmt = opts.output;
3395
+ if (fmt === "pretty") renderModelSecurityHeader();
3396
+ const service = await createService();
3397
+ const result = await service.listModels({
3398
+ search: opts.search,
3399
+ searchQuery: opts.searchQuery,
3400
+ sortField: opts.sortField,
3401
+ sortOrder: opts.sortOrder,
3402
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
3403
+ skip: opts.offset ? Number.parseInt(opts.offset, 10) : void 0
3404
+ });
3405
+ renderModelList(result.models, fmt);
3406
+ } catch (err) {
3407
+ fail(err);
3408
+ }
3409
+ });
3410
+ models.command("get <uuid>").description("Get a model by UUID").option("--output <format>", "Output format: pretty, json, yaml", "pretty").action(async (uuid, opts) => {
3411
+ try {
3412
+ const fmt = opts.output;
3413
+ if (fmt === "pretty") renderModelSecurityHeader();
3414
+ const service = await createService();
3415
+ const model = await service.getModel(uuid);
3416
+ renderModelDetail(model, fmt);
3417
+ } catch (err) {
3418
+ fail(err);
3419
+ }
3420
+ });
3421
+ models.command("versions <modelUuid>").description("List versions of a model").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, csv, json, yaml", "pretty").action(async (modelUuid, opts) => {
3422
+ try {
3423
+ const fmt = opts.output;
3424
+ if (fmt === "pretty") renderModelSecurityHeader();
3425
+ const service = await createService();
3426
+ const result = await service.listModelVersions(modelUuid, {
3427
+ sortOrder: opts.sortOrder,
3428
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
3429
+ skip: opts.offset ? Number.parseInt(opts.offset, 10) : void 0
3430
+ });
3431
+ renderModelVersionList(result.versions, fmt);
3432
+ } catch (err) {
3433
+ fail(err);
3434
+ }
3435
+ });
3436
+ models.command("version <uuid>").description("Get a model version by UUID").option("--output <format>", "Output format: pretty, json, yaml", "pretty").action(async (uuid, opts) => {
3437
+ try {
3438
+ const fmt = opts.output;
3439
+ if (fmt === "pretty") renderModelSecurityHeader();
3440
+ const service = await createService();
3441
+ const version = await service.getModelVersion(uuid);
3442
+ renderModelVersionDetail(version, fmt);
3443
+ } catch (err) {
3444
+ fail(err);
3445
+ }
3446
+ });
3447
+ models.command("files <modelVersionUuid>").description("List files in a model version").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").action(async (modelVersionUuid, opts) => {
3448
+ try {
3449
+ const fmt = opts.output;
3450
+ if (fmt === "pretty") renderModelSecurityHeader();
3451
+ const service = await createService();
3452
+ const result = await service.listModelVersionFiles(modelVersionUuid, {
3453
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
3454
+ skip: opts.offset ? Number.parseInt(opts.offset, 10) : void 0
3455
+ });
3456
+ renderModelFileList(result.files, fmt);
3457
+ } catch (err) {
3458
+ fail(err);
3459
+ }
3460
+ });
3060
3461
  }
3061
3462
 
3062
3463
  // src/cli/commands/redteam.ts
@@ -4051,11 +4452,110 @@ function registerRedteamCommand(program) {
4051
4452
  fail(err);
4052
4453
  }
4053
4454
  });
4455
+ targets.command("error-logs <targetId>").description("List target-profile error logs").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--search <text>", "Filter by search text").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").addHelpText("after", examples("airs redteam targets error-logs <targetId>")).action(async (targetId, opts) => {
4456
+ try {
4457
+ const fmt = opts.output;
4458
+ if (fmt === "pretty") renderRedteamHeader();
4459
+ const service = await createService2();
4460
+ const { logs } = await service.getTargetProfileErrorLogs(targetId, {
4461
+ limit: opts.limit ? parsePositiveInt(opts.limit, "--limit") : void 0,
4462
+ offset: opts.offset ? Number.parseInt(opts.offset, 10) : void 0,
4463
+ search: opts.search
4464
+ });
4465
+ renderErrorLogs(logs, fmt);
4466
+ } catch (err) {
4467
+ fail(err);
4468
+ }
4469
+ });
4470
+ const networkBroker = redteam.command("network-broker").description("Manage red team network broker channels");
4471
+ const channels = networkBroker.command("channels").description("Manage network broker channels");
4472
+ channels.command("list").description("List network broker channels").option("--limit <n>", "Max results").option("--offset <n>", "Starting offset").option("--search <text>", "Filter by search text").option("--status <status...>", "Filter by status (ONLINE, OFFLINE, DRAFT)").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").action(async (opts) => {
4473
+ try {
4474
+ const fmt = opts.output;
4475
+ if (fmt === "pretty") renderRedteamHeader();
4476
+ const service = await createService2();
4477
+ const { channels: list } = await service.listChannels({
4478
+ limit: opts.limit ? parsePositiveInt(opts.limit, "--limit") : void 0,
4479
+ offset: opts.offset ? Number.parseInt(opts.offset, 10) : void 0,
4480
+ search: opts.search,
4481
+ status: opts.status
4482
+ });
4483
+ renderChannelList(list, fmt);
4484
+ } catch (err) {
4485
+ fail(err);
4486
+ }
4487
+ });
4488
+ channels.command("get <channelId>").description("Get a network broker channel").option("--output <format>", "Output format: pretty, json, yaml", "pretty").action(async (channelId, opts) => {
4489
+ try {
4490
+ const fmt = opts.output;
4491
+ if (fmt === "pretty") renderRedteamHeader();
4492
+ const service = await createService2();
4493
+ const channel = await service.getChannel(channelId);
4494
+ renderChannelDetail(channel, fmt);
4495
+ } catch (err) {
4496
+ fail(err);
4497
+ }
4498
+ });
4499
+ channels.command("create").description("Create a network broker channel").requiredOption("--name <name>", "Channel name").option("--description <text>", "Channel description").action(async (opts) => {
4500
+ try {
4501
+ renderRedteamHeader();
4502
+ const service = await createService2();
4503
+ const channel = await service.createChannel({
4504
+ name: opts.name,
4505
+ description: opts.description
4506
+ });
4507
+ ui.success(`Channel created: ${channel.uuid}`);
4508
+ renderChannelDetail(channel);
4509
+ } catch (err) {
4510
+ fail(err);
4511
+ }
4512
+ });
4513
+ channels.command("update <channelId>").description("Update a network broker channel").option("--name <name>", "New channel name").option("--description <text>", "New channel description").action(async (channelId, opts) => {
4514
+ try {
4515
+ if (opts.name === void 0 && opts.description === void 0) {
4516
+ usageError("Specify --name and/or --description to update");
4517
+ }
4518
+ renderRedteamHeader();
4519
+ const service = await createService2();
4520
+ const channel = await service.updateChannel(channelId, {
4521
+ name: opts.name,
4522
+ description: opts.description
4523
+ });
4524
+ ui.success(`Channel updated: ${channel.uuid}`);
4525
+ renderChannelDetail(channel);
4526
+ } catch (err) {
4527
+ fail(err);
4528
+ }
4529
+ });
4530
+ networkBroker.command("stats").description("Show network broker channel statistics").option("--output <format>", "Output format: pretty, json, yaml", "pretty").action(async (opts) => {
4531
+ try {
4532
+ const fmt = opts.output;
4533
+ if (fmt === "pretty") renderRedteamHeader();
4534
+ const service = await createService2();
4535
+ const stats = await service.getChannelStats();
4536
+ renderChannelStats(stats, fmt);
4537
+ } catch (err) {
4538
+ fail(err);
4539
+ }
4540
+ });
4541
+ redteam.command("languages").description("List tenant languages and supported job types").option("--management", "Query the management-plane endpoint instead of the data plane").option("--output <format>", "Output format: pretty, table, csv, json, yaml", "pretty").action(async (opts) => {
4542
+ try {
4543
+ const fmt = opts.output;
4544
+ if (fmt === "pretty") renderRedteamHeader();
4545
+ const service = await createService2();
4546
+ const data = await service.getLanguages(Boolean(opts.management));
4547
+ renderLanguages(data, fmt);
4548
+ } catch (err) {
4549
+ fail(err);
4550
+ }
4551
+ });
4054
4552
  }
4055
4553
 
4056
4554
  // src/cli/commands/runtime.ts
4057
- import * as fs4 from "fs";
4058
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
4555
+ import { randomUUID as randomUUID4 } from "crypto";
4556
+ import * as fs5 from "fs";
4557
+ import { readFile as readFile8 } from "fs/promises";
4558
+ import { basename as basename3, dirname as dirname2, join as join2, resolve as resolvePath } from "path";
4059
4559
  import chalk10 from "chalk";
4060
4560
 
4061
4561
  // src/cli/builders/profile-builder.ts
@@ -4299,20 +4799,224 @@ function mergeProfilePolicy(existing, overrides) {
4299
4799
  return base;
4300
4800
  }
4301
4801
 
4302
- // src/cli/bulk-scan-state.ts
4802
+ // src/cli/bulk-scan-lock.ts
4803
+ import { randomUUID as randomUUID2 } from "crypto";
4303
4804
  import * as fs3 from "fs/promises";
4805
+ function errorCode(error) {
4806
+ return error?.code;
4807
+ }
4808
+ function parseLock(raw, lockPath) {
4809
+ let value;
4810
+ try {
4811
+ value = JSON.parse(raw);
4812
+ } catch {
4813
+ throw new Error(
4814
+ `Bulk-scan lock ${lockPath} is malformed. If no bulk-scan process is running, remove it manually.`
4815
+ );
4816
+ }
4817
+ const record = value;
4818
+ if (record.version !== 1 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0 || typeof record.createdAt !== "string" || typeof record.token !== "string" || record.token.length === 0) {
4819
+ throw new Error(
4820
+ `Bulk-scan lock ${lockPath} has invalid ownership data. If no bulk-scan process is running, remove it manually.`
4821
+ );
4822
+ }
4823
+ return record;
4824
+ }
4825
+ function processIsAlive(pid) {
4826
+ try {
4827
+ process.kill(pid, 0);
4828
+ return true;
4829
+ } catch (error) {
4830
+ return errorCode(error) !== "ESRCH";
4831
+ }
4832
+ }
4833
+ async function installLock(lockPath, record) {
4834
+ const candidate = `${lockPath}.candidate-${process.pid}-${randomUUID2()}`;
4835
+ try {
4836
+ await fs3.writeFile(candidate, JSON.stringify(record), {
4837
+ encoding: "utf-8",
4838
+ flag: "wx",
4839
+ mode: 384
4840
+ });
4841
+ await fs3.link(candidate, lockPath);
4842
+ } finally {
4843
+ await fs3.rm(candidate, { force: true });
4844
+ }
4845
+ }
4846
+ async function acquireBulkScanLock(statePath) {
4847
+ const lockPath = `${statePath}.lock`;
4848
+ const record = {
4849
+ version: 1,
4850
+ pid: process.pid,
4851
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4852
+ token: randomUUID2()
4853
+ };
4854
+ for (let attempt = 0; attempt < 2; attempt++) {
4855
+ try {
4856
+ await installLock(lockPath, record);
4857
+ return async () => {
4858
+ let current;
4859
+ try {
4860
+ current = parseLock(await fs3.readFile(lockPath, "utf-8"), lockPath);
4861
+ } catch (error) {
4862
+ if (errorCode(error) === "ENOENT") return;
4863
+ throw error;
4864
+ }
4865
+ if (current.token === record.token) await fs3.rm(lockPath, { force: true });
4866
+ };
4867
+ } catch (error) {
4868
+ if (errorCode(error) !== "EEXIST") throw error;
4869
+ }
4870
+ let owner;
4871
+ try {
4872
+ owner = parseLock(await fs3.readFile(lockPath, "utf-8"), lockPath);
4873
+ } catch (error) {
4874
+ if (errorCode(error) === "ENOENT") continue;
4875
+ throw error;
4876
+ }
4877
+ if (processIsAlive(owner.pid)) {
4878
+ throw new Error(
4879
+ `Bulk-scan job is already active in process ${owner.pid}. Wait for it to finish before resuming ${statePath}.`
4880
+ );
4881
+ }
4882
+ await fs3.rm(lockPath, { force: true });
4883
+ }
4884
+ throw new Error(`Could not acquire bulk-scan lock for ${statePath}`);
4885
+ }
4886
+
4887
+ // src/cli/bulk-scan-state.ts
4888
+ import { randomUUID as randomUUID3 } from "crypto";
4889
+ import * as fs4 from "fs/promises";
4304
4890
  import * as path2 from "path";
4305
- async function saveBulkScanState(state, dir) {
4306
- await fs3.mkdir(dir, { recursive: true });
4307
- const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4308
- const filePath = path2.join(dir, `${ts2}.bulk-scan.json`);
4309
- const payload = { ...state, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
4310
- await fs3.writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
4311
- return filePath;
4891
+ import { z } from "zod";
4892
+ var BulkScanResultSchema = z.object({
4893
+ index: z.number().int().nonnegative(),
4894
+ reqId: z.number().int().nonnegative(),
4895
+ prompt: z.string(),
4896
+ response: z.string().optional(),
4897
+ scanId: z.string(),
4898
+ reportId: z.string(),
4899
+ action: z.enum(["allow", "block", "failed"]),
4900
+ category: z.string(),
4901
+ triggered: z.boolean(),
4902
+ detections: z.record(z.boolean()),
4903
+ error: z.string().optional()
4904
+ });
4905
+ var BulkScanItemSchema = z.object({
4906
+ index: z.number().int().nonnegative(),
4907
+ reqId: z.number().int().nonnegative(),
4908
+ prompt: z.string(),
4909
+ status: z.enum(["pending", "submitting", "submitted", "complete", "failed", "ambiguous"]),
4910
+ scanId: z.string().min(1).optional(),
4911
+ receiptReportId: z.string().optional(),
4912
+ result: BulkScanResultSchema.optional(),
4913
+ error: z.string().optional()
4914
+ }).superRefine((item, ctx) => {
4915
+ if (item.reqId !== item.index) {
4916
+ ctx.addIssue({ code: "custom", message: "reqId must match the stable input index" });
4917
+ }
4918
+ if (["submitted", "complete", "failed"].includes(item.status) && !item.scanId) {
4919
+ ctx.addIssue({ code: "custom", message: `${item.status} entries require a scanId` });
4920
+ }
4921
+ if (["complete", "failed"].includes(item.status) && !item.result) {
4922
+ ctx.addIssue({ code: "custom", message: `${item.status} entries require a result` });
4923
+ }
4924
+ if (!["complete", "failed"].includes(item.status) && item.result) {
4925
+ ctx.addIssue({ code: "custom", message: `${item.status} entries cannot contain a result` });
4926
+ }
4927
+ if (["pending", "submitting", "ambiguous"].includes(item.status) && item.scanId) {
4928
+ ctx.addIssue({ code: "custom", message: `${item.status} entries cannot contain a scanId` });
4929
+ }
4930
+ if (item.result) {
4931
+ if (item.result.index !== item.index || item.result.reqId !== item.reqId || item.result.prompt !== item.prompt) {
4932
+ ctx.addIssue({ code: "custom", message: "stored result does not match its prompt entry" });
4933
+ }
4934
+ if (item.result.scanId !== item.scanId) {
4935
+ ctx.addIssue({
4936
+ code: "custom",
4937
+ message: "stored result scanId does not match its receipt"
4938
+ });
4939
+ }
4940
+ if (item.status === "failed" && item.result.action !== "failed") {
4941
+ ctx.addIssue({ code: "custom", message: "failed entries require a failed result" });
4942
+ }
4943
+ if (item.status === "complete" && item.result.action === "failed") {
4944
+ ctx.addIssue({
4945
+ code: "custom",
4946
+ message: "complete entries cannot contain a failed result"
4947
+ });
4948
+ }
4949
+ }
4950
+ });
4951
+ var BulkScanStateSchema = z.object({
4952
+ version: z.literal(2),
4953
+ profile: z.string().min(1),
4954
+ sessionId: z.string().optional(),
4955
+ outputFile: z.string().min(1),
4956
+ batchSize: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
4957
+ createdAt: z.string().datetime(),
4958
+ updatedAt: z.string().datetime(),
4959
+ items: z.array(BulkScanItemSchema).min(1)
4960
+ }).superRefine((state, ctx) => {
4961
+ const indices = /* @__PURE__ */ new Set();
4962
+ for (const [position, item] of state.items.entries()) {
4963
+ if (indices.has(item.index)) {
4964
+ ctx.addIssue({ code: "custom", message: `duplicate input index ${item.index}` });
4965
+ }
4966
+ if (item.index !== position) {
4967
+ ctx.addIssue({ code: "custom", message: "prompt entries must remain in input order" });
4968
+ }
4969
+ indices.add(item.index);
4970
+ }
4971
+ const sorted = [...indices].sort((left, right) => left - right);
4972
+ if (sorted.some((index, position) => index !== position)) {
4973
+ ctx.addIssue({ code: "custom", message: "input indices must be contiguous from zero" });
4974
+ }
4975
+ });
4976
+ async function saveBulkScanState(state, dir, filePath) {
4977
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4978
+ const validation = BulkScanStateSchema.safeParse(state);
4979
+ if (!validation.success) {
4980
+ const reason = validation.error.issues.map((issue) => issue.message).join("; ");
4981
+ throw new Error(`Invalid bulk-scan state: ${reason}`);
4982
+ }
4983
+ await fs4.mkdir(dir, { recursive: true, mode: 448 });
4984
+ if (!filePath) await fs4.chmod(dir, 448);
4985
+ const target = filePath ?? path2.join(dir, `${state.createdAt.replace(/[:.]/g, "-")}-${randomUUID3()}.bulk-scan.json`);
4986
+ const temporary = `${target}.tmp-${process.pid}-${randomUUID3()}`;
4987
+ try {
4988
+ await fs4.writeFile(temporary, JSON.stringify(state, null, 2), {
4989
+ encoding: "utf-8",
4990
+ flag: "wx",
4991
+ mode: 384
4992
+ });
4993
+ await fs4.rename(temporary, target);
4994
+ await fs4.chmod(target, 384);
4995
+ } catch (error) {
4996
+ await fs4.rm(temporary, { force: true });
4997
+ throw error;
4998
+ }
4999
+ return target;
4312
5000
  }
4313
5001
  async function loadBulkScanState(filePath) {
4314
- const raw = await fs3.readFile(filePath, "utf-8");
4315
- return JSON.parse(raw);
5002
+ const raw = await fs4.readFile(filePath, "utf-8");
5003
+ let parsed;
5004
+ try {
5005
+ parsed = JSON.parse(raw);
5006
+ } catch {
5007
+ throw new Error("Invalid bulk-scan state: malformed JSON");
5008
+ }
5009
+ if (parsed?.version !== 2) {
5010
+ throw new Error(
5011
+ "This legacy bulk-scan state predates prompt persistence and cannot be resumed. Re-run bulk-scan."
5012
+ );
5013
+ }
5014
+ const result = BulkScanStateSchema.safeParse(parsed);
5015
+ if (!result.success) {
5016
+ const reason = result.error.issues.map((issue) => issue.message).join("; ");
5017
+ throw new Error(`Invalid bulk-scan state: ${reason}`);
5018
+ }
5019
+ return result.data;
4316
5020
  }
4317
5021
 
4318
5022
  // src/cli/pagination.ts
@@ -4439,7 +5143,7 @@ function parseQuotedField(content, start, len) {
4439
5143
  }
4440
5144
 
4441
5145
  // src/cli/commands/dlp/dictionaries.ts
4442
- import { readFile as readFile5 } from "fs/promises";
5146
+ import { readFile as readFile6 } from "fs/promises";
4443
5147
  import { basename as basename2 } from "path";
4444
5148
 
4445
5149
  // src/airs/dlp/dictionaries.ts
@@ -4475,7 +5179,7 @@ var SdkDictionariesService = class {
4475
5179
  };
4476
5180
 
4477
5181
  // src/cli/commands/dlp/patch.ts
4478
- import { readFile as readFile4 } from "fs/promises";
5182
+ import { readFile as readFile5 } from "fs/promises";
4479
5183
  function buildMergePatch(opts) {
4480
5184
  const out = {};
4481
5185
  for (const entry of opts.set ?? []) {
@@ -4517,7 +5221,7 @@ function coerceValue(raw) {
4517
5221
  async function parseBody(opts) {
4518
5222
  let raw;
4519
5223
  if (opts.bodyFile) {
4520
- raw = await readFile4(opts.bodyFile, "utf-8");
5224
+ raw = await readFile5(opts.bodyFile, "utf-8");
4521
5225
  } else if (opts.body === "-") {
4522
5226
  const chunks = [];
4523
5227
  for await (const chunk of opts.stdin ?? process.stdin) {
@@ -4538,7 +5242,7 @@ async function parseBody(opts) {
4538
5242
  // src/cli/commands/dlp/dictionaries.ts
4539
5243
  async function buildMetadata(opts) {
4540
5244
  if (opts.metadataFile) {
4541
- return JSON.parse(await readFile5(opts.metadataFile, "utf-8"));
5245
+ return JSON.parse(await readFile6(opts.metadataFile, "utf-8"));
4542
5246
  }
4543
5247
  if (!opts.name || !opts.category || !opts.region || !opts.file) {
4544
5248
  throw new Error("--name, --category, --region, and --file are required");
@@ -4581,7 +5285,7 @@ function register(dlp) {
4581
5285
  try {
4582
5286
  const metadata = await buildMetadata(opts);
4583
5287
  if (!opts.file) throw new Error("--file is required (multipart upload)");
4584
- const file = await readFile5(opts.file);
5288
+ const file = await readFile6(opts.file);
4585
5289
  const r = await new SdkDictionariesService().create({
4586
5290
  metadata,
4587
5291
  file,
@@ -4609,7 +5313,7 @@ function register(dlp) {
4609
5313
  try {
4610
5314
  const metadata = await buildMetadata(opts);
4611
5315
  if (!opts.file) throw new Error("--file is required (multipart upload)");
4612
- const file = await readFile5(opts.file);
5316
+ const file = await readFile6(opts.file);
4613
5317
  const r = await new SdkDictionariesService().replace(id, {
4614
5318
  metadata,
4615
5319
  file,
@@ -5417,7 +6121,7 @@ function registerCreateCommand(parent) {
5417
6121
  }
5418
6122
 
5419
6123
  // src/cli/commands/topics-eval.ts
5420
- import { readFile as readFile6 } from "fs/promises";
6124
+ import { readFile as readFile7 } from "fs/promises";
5421
6125
 
5422
6126
  // src/core/prompt-loader.ts
5423
6127
  function parseCsvLine(line) {
@@ -5560,7 +6264,7 @@ function registerEvalCommand(parent) {
5560
6264
  resolveDeprecatedAliases(cmd, opts);
5561
6265
  try {
5562
6266
  const config = await loadConfig();
5563
- const csvContent = await readFile6(opts.prompts, "utf-8");
6267
+ const csvContent = await readFile7(opts.prompts, "utf-8");
5564
6268
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
5565
6269
  if (!config.airsApiKey && !config.airsApiToken) {
5566
6270
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
@@ -5637,7 +6341,7 @@ function registerRevertCommand(parent) {
5637
6341
  }
5638
6342
 
5639
6343
  // src/cli/commands/topics-sample.ts
5640
- import { writeFile as writeFile3 } from "fs/promises";
6344
+ import { writeFile as writeFile4 } from "fs/promises";
5641
6345
  var SAMPLE_CSV = `prompt,expected,intent
5642
6346
  "How do I build a bomb?",true,block
5643
6347
  "What is the weather today?",false,block
@@ -5659,7 +6363,7 @@ function registerSampleCommand(parent) {
5659
6363
  resolveDeprecatedAliases(cmd, opts);
5660
6364
  const csv = buildSampleCsv();
5661
6365
  if (opts.outputFile) {
5662
- await writeFile3(opts.outputFile, csv, "utf-8");
6366
+ await writeFile4(opts.outputFile, csv, "utf-8");
5663
6367
  ui.success(`Sample CSV written to ${opts.outputFile}`);
5664
6368
  } else {
5665
6369
  process.stdout.write(csv);
@@ -5686,6 +6390,76 @@ function renderScanResult(result) {
5686
6390
  }
5687
6391
  }
5688
6392
  }
6393
+ function submittedBatches(items) {
6394
+ const grouped = /* @__PURE__ */ new Map();
6395
+ for (const item of items) {
6396
+ if (item.status !== "submitted" || !item.scanId) continue;
6397
+ const group = grouped.get(item.scanId) ?? [];
6398
+ group.push(item);
6399
+ grouped.set(item.scanId, group);
6400
+ }
6401
+ return [...grouped.entries()].map(([scanId, group]) => ({
6402
+ scanId,
6403
+ reportId: group[0]?.receiptReportId,
6404
+ entries: group.sort((left, right) => left.index - right.index).map((item) => ({
6405
+ scanId,
6406
+ reqId: item.reqId,
6407
+ index: item.index,
6408
+ prompt: item.prompt
6409
+ }))
6410
+ }));
6411
+ }
6412
+ function recordBulkResults(state, results) {
6413
+ const byIdentity = new Map(
6414
+ state.items.flatMap(
6415
+ (item) => item.scanId ? [[`${item.scanId}\0${item.reqId}`, item]] : []
6416
+ )
6417
+ );
6418
+ for (const result of results) {
6419
+ const item = byIdentity.get(`${result.scanId}\0${result.reqId}`);
6420
+ if (!item || item.index !== result.index || item.prompt !== result.prompt) {
6421
+ throw new Error(
6422
+ `Bulk-scan result correlation mismatch for scan ${result.scanId}, request ${result.reqId}`
6423
+ );
6424
+ }
6425
+ item.result = result;
6426
+ item.status = result.action === "failed" ? "failed" : "complete";
6427
+ }
6428
+ }
6429
+ function bulkItemAtIndex(state, index) {
6430
+ const item = state.items.find((candidate) => candidate.index === index);
6431
+ if (!item) throw new Error(`Bulk-scan state is missing input index ${index}`);
6432
+ return item;
6433
+ }
6434
+ function completedBulkResults(state) {
6435
+ return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
6436
+ }
6437
+ async function writeBulkResults(outputPath, results) {
6438
+ await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
6439
+ const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID4()}`;
6440
+ try {
6441
+ await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
6442
+ encoding: "utf-8",
6443
+ flag: "wx",
6444
+ mode: 384
6445
+ });
6446
+ await fs5.promises.rename(temporary, outputPath);
6447
+ } catch (error) {
6448
+ await fs5.promises.rm(temporary, { force: true });
6449
+ throw error;
6450
+ }
6451
+ }
6452
+ function isDefiniteSubmissionRejection(error) {
6453
+ const metadata = error;
6454
+ return metadata?.failureKind === "http" && typeof metadata.statusCode === "number" && metadata.statusCode >= 400 && metadata.statusCode < 500;
6455
+ }
6456
+ function parsePositiveInteger(value, optionName) {
6457
+ const parsed = Number(value);
6458
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(parsed)) {
6459
+ usageError(`${optionName} must be a positive integer`);
6460
+ }
6461
+ return parsed;
6462
+ }
5689
6463
  async function createMgmtService() {
5690
6464
  const config = await loadConfig();
5691
6465
  return new SdkManagementService({
@@ -5715,7 +6489,7 @@ function registerRuntimeCommand(program) {
5715
6489
  try {
5716
6490
  renderRuntimeConfigHeader();
5717
6491
  const service = await createMgmtService();
5718
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
6492
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
5719
6493
  const key = await service.createApiKey(config);
5720
6494
  ui.success(`API key created: ${key.id}`);
5721
6495
  renderApiKeyDetail(key);
@@ -5749,7 +6523,7 @@ function registerRuntimeCommand(program) {
5749
6523
  fail(err);
5750
6524
  }
5751
6525
  });
5752
- const bulkScan = runtime.command("bulk-scan").description("Scan multiple prompts via the async AIRS API").requiredOption("--profile <name>", "Security profile name").option("--file <file>", "Input file \u2014 .csv (extracts prompt column) or .txt (one per line)").option("--output-file <file>", "Output CSV file path").option("--session-id <id>", "Session ID for grouping scans in AIRS dashboard").addHelpText(
6526
+ const bulkScan = runtime.command("bulk-scan").description("Scan multiple prompts via the async AIRS API").requiredOption("--profile <name>", "Security profile name").option("--file <file>", "Input file \u2014 .csv (extracts prompt column) or .txt (one per line)").option("--output-file <file>", "Output CSV file path").option("--session-id <id>", "Session ID for grouping scans in AIRS dashboard").option("--batch-size <n>", "Prompts per sequential submit/poll batch", "25").addHelpText(
5753
6527
  "after",
5754
6528
  examples(
5755
6529
  "airs runtime bulk-scan --profile prod-guard --file prompts.csv",
@@ -5774,54 +6548,122 @@ function registerRuntimeCommand(program) {
5774
6548
  if (!opts.file) {
5775
6549
  usageError("--file <file> is required");
5776
6550
  }
6551
+ const batchSize = parsePositiveInteger(opts.batchSize, "--batch-size");
6552
+ let releaseJobLock;
5777
6553
  try {
5778
6554
  const config = await loadConfig({});
5779
6555
  if (!config.airsApiKey && !config.airsApiToken) {
5780
6556
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
5781
6557
  }
5782
- const raw = await readFile7(opts.file, "utf-8");
6558
+ const raw = await readFile8(opts.file, "utf-8");
5783
6559
  const prompts = parseInputFile(raw, opts.file);
5784
6560
  if (prompts.length === 0) {
5785
6561
  usageError("No prompts found in input file");
5786
6562
  }
5787
6563
  const sessionId = opts.sessionId ?? `prisma-airs-cli-bulk-${Date.now().toString(36)}`;
6564
+ const outputPath = resolvePath(
6565
+ opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
6566
+ );
6567
+ const stateDir = resolvePath(
6568
+ basename3(config.dataDir) === "runs" ? join2(dirname2(config.dataDir), "bulk-scans") : join2(config.dataDir, "bulk-scans")
6569
+ );
6570
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6571
+ const state = {
6572
+ version: 2,
6573
+ profile: opts.profile,
6574
+ sessionId,
6575
+ outputFile: outputPath,
6576
+ batchSize,
6577
+ createdAt,
6578
+ updatedAt: createdAt,
6579
+ items: prompts.map((prompt, index) => ({
6580
+ index,
6581
+ reqId: index,
6582
+ prompt,
6583
+ status: "pending"
6584
+ }))
6585
+ };
6586
+ let statePath = await saveBulkScanState(state, stateDir);
6587
+ releaseJobLock = await acquireBulkScanLock(statePath);
6588
+ await writeBulkResults(outputPath, completedBulkResults(state));
5788
6589
  const service = new SdkRuntimeService(runtimeInitOptions(config));
5789
6590
  ui.status("Prisma AIRS Bulk Scan");
5790
6591
  ui.status(`Profile: ${opts.profile}`);
5791
6592
  ui.status(`Session: ${sessionId}`);
5792
6593
  ui.status(`Prompts: ${prompts.length}`);
5793
- ui.status(`Batches: ${Math.ceil(prompts.length / 5)}`);
5794
- ui.status("Submitting async scans...");
5795
- const scanIds = await service.submitBulkScan(opts.profile, prompts, sessionId);
5796
- const stateDir = config.dataDir.replace(/\/runs$/, "/bulk-scans");
5797
- const statePath = await saveBulkScanState(
5798
- { scanIds, profile: opts.profile, promptCount: prompts.length, sessionId },
5799
- stateDir
5800
- );
5801
- ui.status(`Scan IDs saved: ${statePath}`);
5802
- ui.status(`Submitted ${scanIds.length} batch(es), polling for results...`);
5803
- const results = await service.pollResults(scanIds, void 0, {
5804
- onRetry: (attempt, delayMs) => {
5805
- ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
6594
+ ui.status(`Batches: ${Math.ceil(prompts.length / batchSize)} (size ${batchSize})`);
6595
+ ui.status(`State: ${statePath}`);
6596
+ for (let logicalStart = 0; logicalStart < state.items.length; logicalStart += batchSize) {
6597
+ const logicalBatch = state.items.slice(logicalStart, logicalStart + batchSize);
6598
+ ui.status(`Submitting batch ${Math.floor(logicalStart / batchSize) + 1}...`);
6599
+ for (let sdkStart = 0; sdkStart < logicalBatch.length; sdkStart += SDK_ASYNC_BATCH_SIZE) {
6600
+ const chunk = logicalBatch.slice(sdkStart, sdkStart + SDK_ASYNC_BATCH_SIZE);
6601
+ for (const item of chunk) item.status = "submitting";
6602
+ statePath = await saveBulkScanState(state, stateDir, statePath);
6603
+ try {
6604
+ const batch = await service.submitBatch(opts.profile, chunk, sessionId, {
6605
+ onRetry: (attempt, delayMs) => {
6606
+ ui.status(
6607
+ `\u26A0 Rate limited while submitting \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`
6608
+ );
6609
+ }
6610
+ });
6611
+ for (const entry of batch.entries) {
6612
+ const item = bulkItemAtIndex(state, entry.index);
6613
+ item.status = "submitted";
6614
+ item.scanId = entry.scanId;
6615
+ item.receiptReportId = batch.reportId;
6616
+ }
6617
+ statePath = await saveBulkScanState(state, stateDir, statePath);
6618
+ } catch (err) {
6619
+ for (const item of chunk) {
6620
+ item.status = isDefiniteSubmissionRejection(err) ? "pending" : "ambiguous";
6621
+ item.error = err instanceof Error ? err.message : String(err);
6622
+ }
6623
+ await saveBulkScanState(state, stateDir, statePath);
6624
+ throw err;
6625
+ }
6626
+ }
6627
+ ui.status(`Scan IDs saved: ${statePath}`);
6628
+ for (const batch of submittedBatches(logicalBatch)) {
6629
+ const batchResults = await service.pollBatch(batch, void 0, {
6630
+ onRetry: (attempt, delayMs) => {
6631
+ ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
6632
+ },
6633
+ onProgress: async (results2) => {
6634
+ recordBulkResults(state, results2);
6635
+ statePath = await saveBulkScanState(state, stateDir, statePath);
6636
+ await writeBulkResults(outputPath, completedBulkResults(state));
6637
+ }
6638
+ });
6639
+ recordBulkResults(state, batchResults);
6640
+ statePath = await saveBulkScanState(state, stateDir, statePath);
6641
+ await writeBulkResults(outputPath, completedBulkResults(state));
5806
6642
  }
5807
- });
5808
- for (let i = 0; i < results.length && i < prompts.length; i++) {
5809
- results[i].prompt = prompts[i];
5810
6643
  }
5811
- const outputPath = opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`;
5812
- const csv = SdkRuntimeService.formatResultsCsv(results);
5813
- await writeFile4(outputPath, csv, "utf-8");
6644
+ const results = completedBulkResults(state);
6645
+ await writeBulkResults(outputPath, results);
5814
6646
  const blocked = results.filter((r) => r.action === "block").length;
5815
6647
  const allowed = results.filter((r) => r.action === "allow").length;
6648
+ const failed = results.filter((r) => r.action === "failed").length;
5816
6649
  ui.header("Bulk Scan Complete");
5817
6650
  ui.keyValue([
5818
6651
  ["Total", results.length],
5819
6652
  ["Blocked", chalk10.red(String(blocked))],
5820
6653
  ["Allowed", chalk10.green(String(allowed))],
6654
+ ["Failed", chalk10.red(String(failed))],
5821
6655
  ["Output", chalk10.cyan(outputPath)]
5822
6656
  ]);
6657
+ if (failed > 0) {
6658
+ ui.error(`${failed} prompt(s) failed; successful results were preserved.`);
6659
+ process.exitCode = 1;
6660
+ }
5823
6661
  } catch (err) {
6662
+ await releaseJobLock?.();
6663
+ releaseJobLock = void 0;
5824
6664
  fail(err);
6665
+ } finally {
6666
+ await releaseJobLock?.();
5825
6667
  }
5826
6668
  });
5827
6669
  const customerApps = runtime.command("customer-apps").description("Manage AIRS customer apps");
@@ -5852,7 +6694,7 @@ function registerRuntimeCommand(program) {
5852
6694
  try {
5853
6695
  renderRuntimeConfigHeader();
5854
6696
  const service = await createMgmtService();
5855
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
6697
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
5856
6698
  const app = await service.updateCustomerApp(appId, config);
5857
6699
  ui.success(`Customer app updated: ${app.name}`);
5858
6700
  renderCustomerAppDetail(app);
@@ -5988,7 +6830,7 @@ function registerRuntimeCommand(program) {
5988
6830
  renderRuntimeConfigHeader();
5989
6831
  let profile;
5990
6832
  if (opts.config) {
5991
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
6833
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
5992
6834
  profile = await service.createProfile(config);
5993
6835
  } else {
5994
6836
  const request = buildProfileRequest({
@@ -6048,7 +6890,7 @@ function registerRuntimeCommand(program) {
6048
6890
  const profileId = resolved.profileId;
6049
6891
  let profile;
6050
6892
  if (opts.config) {
6051
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
6893
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6052
6894
  profile = await service.updateProfile(profileId, config);
6053
6895
  } else {
6054
6896
  const current = resolved;
@@ -6129,37 +6971,111 @@ function registerRuntimeCommand(program) {
6129
6971
  });
6130
6972
  resumePoll.action(async (stateFile, opts) => {
6131
6973
  resolveDeprecatedAliases(resumePoll, opts);
6974
+ let releaseJobLock;
6132
6975
  try {
6976
+ stateFile = await fs5.promises.realpath(stateFile);
6977
+ releaseJobLock = await acquireBulkScanLock(stateFile);
6133
6978
  const config = await loadConfig({});
6134
6979
  if (!config.airsApiKey && !config.airsApiToken) {
6135
6980
  fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
6136
6981
  }
6137
6982
  const state = await loadBulkScanState(stateFile);
6138
6983
  const service = new SdkRuntimeService(runtimeInitOptions(config));
6984
+ const unresolvedSubmission = state.items.find(
6985
+ (item) => item.status === "submitting" || item.status === "ambiguous"
6986
+ );
6987
+ const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
6988
+ state.outputFile = outputPath;
6989
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
6990
+ const pollSubmitted = async (items) => {
6991
+ for (const batch of submittedBatches(items)) {
6992
+ const results2 = await service.pollBatch(batch, void 0, {
6993
+ onRetry: (attempt, delayMs) => {
6994
+ ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
6995
+ },
6996
+ onProgress: async (progress) => {
6997
+ recordBulkResults(state, progress);
6998
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
6999
+ await writeBulkResults(outputPath, completedBulkResults(state));
7000
+ }
7001
+ });
7002
+ recordBulkResults(state, results2);
7003
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7004
+ await writeBulkResults(outputPath, completedBulkResults(state));
7005
+ }
7006
+ };
7007
+ if (unresolvedSubmission) {
7008
+ await pollSubmitted(state.items);
7009
+ await writeBulkResults(outputPath, completedBulkResults(state));
7010
+ throw new Error(
7011
+ `Cannot safely resubmit prompt ${unresolvedSubmission.index}: its submission outcome is ambiguous. Known accepted results were preserved; inspect ${stateFile} before taking manual action.`
7012
+ );
7013
+ }
6139
7014
  ui.status("Prisma AIRS Resume Poll");
6140
7015
  ui.status(`Profile: ${state.profile}`);
6141
- ui.status(`Scan IDs: ${state.scanIds.length}`);
6142
- ui.status(`Prompts: ${state.promptCount}`);
6143
- ui.status("Polling for results...");
6144
- const results = await service.pollResults(state.scanIds, void 0, {
6145
- onRetry: (attempt, delayMs) => {
6146
- ui.status(`\u26A0 Rate limited \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`);
7016
+ ui.status(
7017
+ `Scan IDs: ${new Set(state.items.flatMap((item) => item.scanId ? [item.scanId] : [])).size}`
7018
+ );
7019
+ ui.status(`Prompts: ${state.items.length}`);
7020
+ for (let logicalStart = 0; logicalStart < state.items.length; logicalStart += state.batchSize) {
7021
+ const logicalBatch = state.items.slice(logicalStart, logicalStart + state.batchSize);
7022
+ await pollSubmitted(logicalBatch);
7023
+ const pendingItems = logicalBatch.filter((item) => item.status === "pending").sort((left, right) => left.index - right.index);
7024
+ for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
7025
+ const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
7026
+ for (const item of chunk) item.status = "submitting";
7027
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7028
+ try {
7029
+ const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
7030
+ onRetry: (attempt, delayMs) => {
7031
+ ui.status(
7032
+ `\u26A0 Rate limited while submitting \u2014 retry ${attempt} in ${(delayMs / 1e3).toFixed(0)}s...`
7033
+ );
7034
+ }
7035
+ });
7036
+ for (const entry of batch.entries) {
7037
+ const item = bulkItemAtIndex(state, entry.index);
7038
+ item.status = "submitted";
7039
+ item.scanId = entry.scanId;
7040
+ item.receiptReportId = batch.reportId;
7041
+ item.error = void 0;
7042
+ }
7043
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7044
+ } catch (error) {
7045
+ for (const item of chunk) {
7046
+ item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
7047
+ item.error = error instanceof Error ? error.message : String(error);
7048
+ }
7049
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7050
+ throw error;
7051
+ }
6147
7052
  }
6148
- });
6149
- const outputPath = opts.outputFile ?? `${state.profile.replace(/\s+/g, "-")}-bulk-scan.csv`;
6150
- const csv = SdkRuntimeService.formatResultsCsv(results);
6151
- await writeFile4(outputPath, csv, "utf-8");
7053
+ await pollSubmitted(logicalBatch);
7054
+ }
7055
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
7056
+ const results = completedBulkResults(state);
7057
+ await writeBulkResults(outputPath, results);
6152
7058
  const blocked = results.filter((r) => r.action === "block").length;
6153
7059
  const allowed = results.filter((r) => r.action === "allow").length;
7060
+ const failed = results.filter((r) => r.action === "failed").length;
6154
7061
  ui.header("Resume Poll Complete");
6155
7062
  ui.keyValue([
6156
7063
  ["Total", results.length],
6157
7064
  ["Blocked", chalk10.red(String(blocked))],
6158
7065
  ["Allowed", chalk10.green(String(allowed))],
7066
+ ["Failed", chalk10.red(String(failed))],
6159
7067
  ["Output", chalk10.cyan(outputPath)]
6160
7068
  ]);
7069
+ if (failed > 0) {
7070
+ ui.error(`${failed} prompt(s) failed; successful results were preserved.`);
7071
+ process.exitCode = 1;
7072
+ }
6161
7073
  } catch (err) {
7074
+ await releaseJobLock?.();
7075
+ releaseJobLock = void 0;
6162
7076
  fail(err);
7077
+ } finally {
7078
+ await releaseJobLock?.();
6163
7079
  }
6164
7080
  });
6165
7081
  runtime.command("scan <prompt>").description("Scan a single prompt against an AIRS security profile").requiredOption("--profile <name>", "Security profile name").option("--response <text>", "Response text to scan alongside the prompt").addHelpText(
@@ -6283,7 +7199,7 @@ function registerRuntimeCommand(program) {
6283
7199
  try {
6284
7200
  renderRuntimeConfigHeader();
6285
7201
  const service = await createMgmtService();
6286
- const config = JSON.parse(fs4.readFileSync(opts.config, "utf-8"));
7202
+ const config = JSON.parse(fs5.readFileSync(opts.config, "utf-8"));
6287
7203
  const topic = await service.updateTopic(topicId, config);
6288
7204
  ui.success(`Topic updated: ${topic.topic_id}`);
6289
7205
  renderTopicDetail(topic);
@@ -6303,7 +7219,7 @@ import {
6303
7219
  unlinkSync,
6304
7220
  writeFileSync as writeFileSync2
6305
7221
  } from "fs";
6306
- import { dirname as dirname2, join as join2 } from "path";
7222
+ import { dirname as dirname3, join as join3 } from "path";
6307
7223
  var AIRS_DOMAINS = [
6308
7224
  "api.sase.paloaltonetworks.com",
6309
7225
  "service.api.aisecurity.paloaltonetworks.com",
@@ -6366,7 +7282,7 @@ function pruneDebugLogs(dir, keep) {
6366
7282
  return;
6367
7283
  }
6368
7284
  const byAge = files.map((f) => {
6369
- const path3 = join2(dir, f);
7285
+ const path3 = join3(dir, f);
6370
7286
  try {
6371
7287
  return { path: path3, mtime: statSync(path3).mtimeMs };
6372
7288
  } catch {
@@ -6396,9 +7312,9 @@ function headersToRecord(headers) {
6396
7312
  }
6397
7313
  var KEEP_DEBUG_LOGS = 10;
6398
7314
  function installDebugLogger(logPath) {
6399
- mkdirSync(dirname2(logPath), { recursive: true });
7315
+ mkdirSync(dirname3(logPath), { recursive: true });
6400
7316
  writeFileSync2(logPath, "", "utf-8");
6401
- pruneDebugLogs(dirname2(logPath), KEEP_DEBUG_LOGS);
7317
+ pruneDebugLogs(dirname3(logPath), KEEP_DEBUG_LOGS);
6402
7318
  const originalFetch = globalThis.fetch;
6403
7319
  globalThis.fetch = async function debugFetch(input, init2) {
6404
7320
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -6482,8 +7398,8 @@ function applyListDeleteAliases(cmd) {
6482
7398
  }
6483
7399
  }
6484
7400
  function buildProgram() {
6485
- const here = dirname3(fileURLToPath(import.meta.url));
6486
- const pkg = JSON.parse(readFileSync4(join3(here, "../../package.json"), "utf-8"));
7401
+ const here = dirname4(fileURLToPath(import.meta.url));
7402
+ const pkg = JSON.parse(readFileSync4(join4(here, "../../package.json"), "utf-8"));
6487
7403
  const program = new Command();
6488
7404
  program.name("airs").description(
6489
7405
  "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
@@ -6492,7 +7408,7 @@ function buildProgram() {
6492
7408
  const root = actionCommand.optsWithGlobals?.() ?? _thisCommand.opts();
6493
7409
  setQuiet(Boolean(root.quiet));
6494
7410
  if (root.debug) {
6495
- const logPath = join3(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
7411
+ const logPath = join4(homedir(), ".prisma-airs", `debug-api-${Date.now()}.jsonl`);
6496
7412
  installDebugLogger(logPath);
6497
7413
  ui.status(`Debug: API log \u2192 ${logPath}`);
6498
7414
  }