@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/README.md +3 -1
- package/dist/{chunk-JXHYQFEK.js → chunk-TTBN7YHC.js} +558 -39
- package/dist/cli/index.js +984 -68
- package/dist/index.d.ts +264 -13
- package/dist/index.js +3 -1
- package/package.json +2 -2
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-
|
|
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
|
|
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
|
|
4058
|
-
import
|
|
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-
|
|
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
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
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
|
|
4315
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 /
|
|
5794
|
-
ui.status(
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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
|
|
5812
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
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
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
7315
|
+
mkdirSync(dirname3(logPath), { recursive: true });
|
|
6400
7316
|
writeFileSync2(logPath, "", "utf-8");
|
|
6401
|
-
pruneDebugLogs(
|
|
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 =
|
|
6486
|
-
const pkg = JSON.parse(readFileSync4(
|
|
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 =
|
|
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
|
}
|