@cdot65/prisma-airs-cli 5.11.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/{chunk-37SN57XQ.js → chunk-KGMQY2IT.js} +46 -108
- package/dist/cli/index.js +513 -413
- package/dist/index.d.ts +11 -12
- package/dist/index.js +1 -1
- package/package.json +2 -3
package/dist/cli/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AirsScanService,
|
|
4
4
|
ConfigSchema,
|
|
5
|
+
RETIRED_CONFIG_KEYS,
|
|
5
6
|
RateLimitedScanService,
|
|
6
7
|
SDK_ASYNC_BATCH_SIZE,
|
|
7
8
|
SdkManagementService,
|
|
@@ -14,9 +15,7 @@ import {
|
|
|
14
15
|
collectRuntimeDailyReport,
|
|
15
16
|
computeMetrics,
|
|
16
17
|
createTenant,
|
|
17
|
-
defaultTenantConfigPath,
|
|
18
18
|
deleteTenant,
|
|
19
|
-
expandConfigPath,
|
|
20
19
|
getOrCreateManagementClient,
|
|
21
20
|
inspectConfig,
|
|
22
21
|
loadConfig,
|
|
@@ -31,7 +30,7 @@ import {
|
|
|
31
30
|
renderRedTeamReportMarkdown,
|
|
32
31
|
renderRuntimeReportHtml,
|
|
33
32
|
renderRuntimeReportMarkdown,
|
|
34
|
-
|
|
33
|
+
resolveConfigContext,
|
|
35
34
|
resolveOutputDir,
|
|
36
35
|
sanitizeFilename,
|
|
37
36
|
switchTenant,
|
|
@@ -40,10 +39,7 @@ import {
|
|
|
40
39
|
validateTopic,
|
|
41
40
|
writeBackupFile,
|
|
42
41
|
writeReportFile
|
|
43
|
-
} from "../chunk-
|
|
44
|
-
|
|
45
|
-
// src/cli/index.ts
|
|
46
|
-
import "dotenv/config";
|
|
42
|
+
} from "../chunk-KGMQY2IT.js";
|
|
47
43
|
|
|
48
44
|
// src/cli/process-guards.ts
|
|
49
45
|
import chalk from "chalk";
|
|
@@ -61,7 +57,7 @@ function installProcessGuards() {
|
|
|
61
57
|
// src/cli/program.ts
|
|
62
58
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
63
59
|
import { readFileSync as readFileSync4 } from "fs";
|
|
64
|
-
import { dirname as
|
|
60
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
65
61
|
import { fileURLToPath } from "url";
|
|
66
62
|
import { Command } from "commander";
|
|
67
63
|
|
|
@@ -79,65 +75,124 @@ import { dump as dump3 } from "js-yaml";
|
|
|
79
75
|
import { z } from "zod";
|
|
80
76
|
|
|
81
77
|
// src/config/client-options.ts
|
|
82
|
-
|
|
78
|
+
import {
|
|
79
|
+
AGENT_GUARD_DATA_ENDPOINT,
|
|
80
|
+
AGENT_GUARD_MGMT_ENDPOINT,
|
|
81
|
+
DEFAULT_AI_GW_ADMIN_ENDPOINT,
|
|
82
|
+
DEFAULT_AI_GW_DATA_ENDPOINT,
|
|
83
|
+
DEFAULT_DLP_ENDPOINT,
|
|
84
|
+
DEFAULT_ENDPOINT,
|
|
85
|
+
DEFAULT_IAM_ENDPOINT,
|
|
86
|
+
DEFAULT_MGMT_ENDPOINT,
|
|
87
|
+
DEFAULT_MODEL_SEC_DATA_ENDPOINT,
|
|
88
|
+
DEFAULT_MODEL_SEC_MGMT_ENDPOINT,
|
|
89
|
+
DEFAULT_RED_TEAM_DATA_ENDPOINT,
|
|
90
|
+
DEFAULT_RED_TEAM_MGMT_ENDPOINT,
|
|
91
|
+
DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT,
|
|
92
|
+
DEFAULT_TOKEN_ENDPOINT
|
|
93
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
94
|
+
|
|
95
|
+
// src/config/credentials.ts
|
|
96
|
+
var MANAGEMENT_CREDENTIAL_KEYS = [
|
|
97
|
+
"mgmtClientId",
|
|
98
|
+
"mgmtClientSecret",
|
|
99
|
+
"mgmtTsgId"
|
|
100
|
+
];
|
|
101
|
+
var SCANNER_CREDENTIAL_KEYS = ["airsApiKey", "airsApiToken"];
|
|
102
|
+
function present(value) {
|
|
103
|
+
return typeof value === "string" && value.trim() !== "";
|
|
104
|
+
}
|
|
105
|
+
function missingManagementCredentials(config) {
|
|
106
|
+
return MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !present(config[key]));
|
|
107
|
+
}
|
|
108
|
+
function hasScannerCredentials(config) {
|
|
109
|
+
return SCANNER_CREDENTIAL_KEYS.some((key) => present(config[key]));
|
|
110
|
+
}
|
|
111
|
+
function safeContext() {
|
|
112
|
+
try {
|
|
113
|
+
return resolveConfigContext();
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function settingRemedy(keys, context3 = safeContext()) {
|
|
119
|
+
const list = keys.join(", ");
|
|
120
|
+
if (context3?.selection === "tenant") {
|
|
121
|
+
return `Run 'airs tenant set ${context3.tenant.name} <key>' for ${list} (secrets prompt hidden, or use --stdin)`;
|
|
122
|
+
}
|
|
123
|
+
if (context3?.selection === "explicit") return `Add ${list} to ${context3.path}`;
|
|
124
|
+
return `Run 'airs tenant create <name>' (prompts for the OAuth credentials), then 'airs tenant switch <name>'`;
|
|
125
|
+
}
|
|
126
|
+
function assertManagementCredentials(config, context3) {
|
|
127
|
+
const missing = missingManagementCredentials(config);
|
|
128
|
+
if (missing.length === 0) return;
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Management credentials are not configured (missing ${missing.join(", ")}). ${settingRemedy(missing, context3)}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function assertScannerCredentials(config, context3) {
|
|
134
|
+
if (hasScannerCredentials(config)) return;
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Scanner credentials are not configured (airsApiKey or airsApiToken). ${settingRemedy(["airsApiKey"], context3)}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/config/client-options.ts
|
|
141
|
+
var DEFAULT_MGMT_DASHBOARD_ENDPOINT = "https://api.apps.paloaltonetworks.com/aisec";
|
|
142
|
+
function oauth(config) {
|
|
143
|
+
assertManagementCredentials(config);
|
|
83
144
|
return {
|
|
84
145
|
clientId: config.mgmtClientId,
|
|
85
146
|
clientSecret: config.mgmtClientSecret,
|
|
86
147
|
tsgId: config.mgmtTsgId,
|
|
87
|
-
|
|
88
|
-
mgmtEndpoint: config.agentGuardMgmtEndpoint,
|
|
89
|
-
tokenEndpoint: config.agentGuardTokenEndpoint ?? config.mgmtTokenEndpoint
|
|
148
|
+
tokenEndpoint: config.mgmtTokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT
|
|
90
149
|
};
|
|
91
150
|
}
|
|
92
151
|
function managementClientOptions(config) {
|
|
93
152
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
dashboardEndpoint: config.mgmtDashboardEndpoint ?? "https://api.apps.paloaltonetworks.com/aisec",
|
|
99
|
-
tokenEndpoint: config.mgmtTokenEndpoint,
|
|
100
|
-
dlpEndpoint: config.dlpEndpoint
|
|
153
|
+
...oauth(config),
|
|
154
|
+
apiEndpoint: config.mgmtEndpoint ?? DEFAULT_MGMT_ENDPOINT,
|
|
155
|
+
dashboardEndpoint: config.mgmtDashboardEndpoint ?? DEFAULT_MGMT_DASHBOARD_ENDPOINT,
|
|
156
|
+
dlpEndpoint: config.dlpEndpoint ?? DEFAULT_DLP_ENDPOINT
|
|
101
157
|
};
|
|
102
158
|
}
|
|
103
|
-
function
|
|
159
|
+
function agentGuardClientOptions(config) {
|
|
104
160
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
numRetries: config.airsNumRetries
|
|
161
|
+
...oauth(config),
|
|
162
|
+
dataEndpoint: config.agentGuardDataEndpoint ?? AGENT_GUARD_DATA_ENDPOINT,
|
|
163
|
+
mgmtEndpoint: config.agentGuardMgmtEndpoint ?? AGENT_GUARD_MGMT_ENDPOINT
|
|
109
164
|
};
|
|
110
165
|
}
|
|
111
166
|
function redTeamClientOptions(config) {
|
|
112
167
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
...oauth(config),
|
|
169
|
+
dataEndpoint: config.redTeamDataEndpoint ?? DEFAULT_RED_TEAM_DATA_ENDPOINT,
|
|
170
|
+
mgmtEndpoint: config.redTeamMgmtEndpoint ?? DEFAULT_RED_TEAM_MGMT_ENDPOINT,
|
|
171
|
+
networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint ?? DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function modelSecurityClientOptions(config) {
|
|
175
|
+
return {
|
|
176
|
+
...oauth(config),
|
|
177
|
+
dataEndpoint: config.modelSecDataEndpoint ?? DEFAULT_MODEL_SEC_DATA_ENDPOINT,
|
|
178
|
+
mgmtEndpoint: config.modelSecMgmtEndpoint ?? DEFAULT_MODEL_SEC_MGMT_ENDPOINT
|
|
120
179
|
};
|
|
121
180
|
}
|
|
122
181
|
function aiGatewayClientOptions(config) {
|
|
123
182
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
adminEndpoint: config.aiGwAdminEndpoint,
|
|
129
|
-
iamEndpoint: config.iamEndpoint,
|
|
130
|
-
tokenEndpoint: config.aiGwTokenEndpoint ?? config.mgmtTokenEndpoint
|
|
183
|
+
...oauth(config),
|
|
184
|
+
dataEndpoint: config.aiGwDataEndpoint ?? DEFAULT_AI_GW_DATA_ENDPOINT,
|
|
185
|
+
adminEndpoint: config.aiGwAdminEndpoint ?? DEFAULT_AI_GW_ADMIN_ENDPOINT,
|
|
186
|
+
iamEndpoint: config.iamEndpoint ?? DEFAULT_IAM_ENDPOINT
|
|
131
187
|
};
|
|
132
188
|
}
|
|
133
|
-
function
|
|
189
|
+
function runtimeInitOptions(config) {
|
|
190
|
+
assertScannerCredentials(config);
|
|
134
191
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
mgmtEndpoint: config.modelSecMgmtEndpoint,
|
|
140
|
-
tokenEndpoint: config.modelSecTokenEndpoint ?? config.mgmtTokenEndpoint
|
|
192
|
+
apiKey: config.airsApiKey,
|
|
193
|
+
apiToken: config.airsApiToken,
|
|
194
|
+
apiEndpoint: config.airsApiEndpoint ?? DEFAULT_ENDPOINT,
|
|
195
|
+
numRetries: config.airsNumRetries
|
|
141
196
|
};
|
|
142
197
|
}
|
|
143
198
|
|
|
@@ -384,13 +439,7 @@ async function resolveOutput(command, opts, resolution = {}) {
|
|
|
384
439
|
while (rootCommand.parent) rootCommand = rootCommand.parent;
|
|
385
440
|
const globalIsExplicit = rootCommand.getOptionValueSource?.("output") === "cli";
|
|
386
441
|
const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
|
|
390
|
-
} catch (error) {
|
|
391
|
-
if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
|
|
392
|
-
else throw error;
|
|
393
|
-
}
|
|
442
|
+
const configured = resolution.ignoreConfig ? void 0 : (await loadConfig()).defaultOutput;
|
|
394
443
|
const candidate = String(
|
|
395
444
|
localIsExplicit ? opts.output : globalOutput ?? configured ?? "pretty"
|
|
396
445
|
);
|
|
@@ -5360,7 +5409,7 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5360
5409
|
title: opts.title,
|
|
5361
5410
|
maxPages: maxPages3,
|
|
5362
5411
|
workspace: opts.workspace,
|
|
5363
|
-
tsgId: config.mgmtTsgId ??
|
|
5412
|
+
tsgId: config.mgmtTsgId ?? "",
|
|
5364
5413
|
start,
|
|
5365
5414
|
end
|
|
5366
5415
|
});
|
|
@@ -5824,171 +5873,61 @@ function registerCompletionCommand(program) {
|
|
|
5824
5873
|
});
|
|
5825
5874
|
}
|
|
5826
5875
|
|
|
5827
|
-
// src/cli/commands/
|
|
5828
|
-
import {
|
|
5829
|
-
import {
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
}
|
|
5838
|
-
function maskSecret(value) {
|
|
5839
|
-
const str = value == null ? "" : String(value);
|
|
5840
|
-
return str.length >= 8 ? `***${str.slice(-4)}` : "***";
|
|
5841
|
-
}
|
|
5842
|
-
function buildConfigRows(inspected, reveal) {
|
|
5843
|
-
return Object.entries(inspected).map(([key, entry]) => {
|
|
5844
|
-
const raw = entry.value == null ? "" : String(entry.value);
|
|
5845
|
-
const value = raw !== "" && isSecretKey(key) && !reveal ? maskSecret(raw) : raw;
|
|
5846
|
-
return { key, value, source: entry.source };
|
|
5847
|
-
});
|
|
5848
|
-
}
|
|
5849
|
-
async function readConfigFileStrict(filePath) {
|
|
5850
|
-
let raw;
|
|
5851
|
-
try {
|
|
5852
|
-
raw = await readFile3(filePath, "utf-8");
|
|
5853
|
-
} catch {
|
|
5854
|
-
return { ok: true, data: {} };
|
|
5855
|
-
}
|
|
5856
|
-
try {
|
|
5857
|
-
const parsed = JSON.parse(raw);
|
|
5858
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5859
|
-
return { ok: false, error: `Config file is not a JSON object: ${filePath}` };
|
|
5860
|
-
}
|
|
5861
|
-
return { ok: true, data: parsed };
|
|
5862
|
-
} catch {
|
|
5863
|
-
return { ok: false, error: `Config file is not valid JSON: ${filePath}` };
|
|
5864
|
-
}
|
|
5865
|
-
}
|
|
5866
|
-
async function setConfigValue(filePath, key, value) {
|
|
5867
|
-
const read = await readConfigFileStrict(filePath);
|
|
5868
|
-
if (!read.ok) return read;
|
|
5869
|
-
const candidate = { ...read.data, [key]: value };
|
|
5870
|
-
const result = ConfigSchema.safeParse(candidate);
|
|
5871
|
-
if (!result.success) {
|
|
5872
|
-
const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
5873
|
-
return { ok: false, error: messages.join("; ") };
|
|
5874
|
-
}
|
|
5875
|
-
const coerced = result.data[key];
|
|
5876
|
-
const next = { ...read.data, [key]: coerced };
|
|
5877
|
-
await mkdir(dirname2(filePath), { recursive: true });
|
|
5878
|
-
await writeFile(filePath, `${JSON.stringify(next, null, 2)}
|
|
5879
|
-
`, "utf-8");
|
|
5880
|
-
return { ok: true, value: coerced };
|
|
5881
|
-
}
|
|
5882
|
-
async function unsetConfigValue(filePath, key) {
|
|
5883
|
-
const read = await readConfigFileStrict(filePath);
|
|
5884
|
-
if (!read.ok) return read;
|
|
5885
|
-
if (!(key in read.data)) return { ok: true, removed: false };
|
|
5886
|
-
const next = { ...read.data };
|
|
5887
|
-
delete next[key];
|
|
5888
|
-
await writeFile(filePath, `${JSON.stringify(next, null, 2)}
|
|
5889
|
-
`, "utf-8");
|
|
5890
|
-
return { ok: true, removed: true };
|
|
5891
|
-
}
|
|
5892
|
-
function assertKnownKey(key) {
|
|
5893
|
-
if (!isKnownKey(key)) {
|
|
5894
|
-
usageError(`Unknown config key '${key}'. Valid keys: ${CONFIG_KEYS.join(", ")}`);
|
|
5895
|
-
}
|
|
5896
|
-
}
|
|
5897
|
-
var COLUMNS = [
|
|
5898
|
-
{ key: "key", label: "Key" },
|
|
5899
|
-
{ key: "value", label: "Value" },
|
|
5900
|
-
{ key: "source", label: "Source" }
|
|
5876
|
+
// src/cli/commands/doctor.ts
|
|
5877
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5878
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5879
|
+
import { init, Scanner } from "@cdot65/prisma-airs-sdk";
|
|
5880
|
+
|
|
5881
|
+
// src/config/env.ts
|
|
5882
|
+
var SDK_DIAGNOSTIC_ENV_VARS = [
|
|
5883
|
+
"PANW_AI_SEC_DEBUG",
|
|
5884
|
+
"PANW_AI_SEC_DEBUG_BODY",
|
|
5885
|
+
"PANW_AI_SEC_TIMEOUT_MS"
|
|
5901
5886
|
];
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
} else {
|
|
5922
|
-
console.log(formatOutput(rows, COLUMNS, fmt));
|
|
5923
|
-
}
|
|
5924
|
-
} catch (err) {
|
|
5925
|
-
fail(err);
|
|
5926
|
-
}
|
|
5927
|
-
});
|
|
5928
|
-
const configGet = config.command("get <key>").description("Print a single effective config value").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show the real value of a secret key").action(async (key, opts) => {
|
|
5929
|
-
try {
|
|
5930
|
-
assertKnownKey(key);
|
|
5931
|
-
const inspected = await inspectConfig();
|
|
5932
|
-
const entry = inspected[key];
|
|
5933
|
-
const raw = entry.value == null ? "" : String(entry.value);
|
|
5934
|
-
const fmt = await resolveOutput(configGet, opts);
|
|
5935
|
-
const value = raw !== "" && isSecretKey(key) && !opts.reveal ? maskSecret(raw) : raw;
|
|
5936
|
-
if (fmt !== "pretty") {
|
|
5937
|
-
console.log(formatOutput([{ key, value, source: entry.source }], COLUMNS, fmt));
|
|
5938
|
-
return;
|
|
5939
|
-
}
|
|
5940
|
-
if (raw !== "" && isSecretKey(key)) {
|
|
5941
|
-
if (opts.reveal) {
|
|
5942
|
-
ui.status(`Warning: printing secret value for '${key}'`);
|
|
5943
|
-
console.log(raw);
|
|
5944
|
-
} else {
|
|
5945
|
-
console.log(maskSecret(raw));
|
|
5946
|
-
}
|
|
5947
|
-
} else {
|
|
5948
|
-
console.log(raw);
|
|
5949
|
-
}
|
|
5950
|
-
} catch (err) {
|
|
5951
|
-
fail(err);
|
|
5952
|
-
}
|
|
5953
|
-
});
|
|
5954
|
-
config.command("set <key> <value>").description("Set a config value in the config file (validated via schema)").action(async (key, value) => {
|
|
5955
|
-
try {
|
|
5956
|
-
assertKnownKey(key);
|
|
5957
|
-
const filePath = resolveConfigFilePath();
|
|
5958
|
-
const result = await setConfigValue(filePath, key, value);
|
|
5959
|
-
if (!result.ok) usageError(result.error);
|
|
5960
|
-
const display = isSecretKey(key) ? maskSecret(result.value) : String(result.value);
|
|
5961
|
-
ui.success(`Set ${key} = ${display} in ${filePath}`);
|
|
5962
|
-
} catch (err) {
|
|
5963
|
-
fail(err);
|
|
5964
|
-
}
|
|
5965
|
-
});
|
|
5966
|
-
config.command("unset <key>").description("Remove a key from the config file (defaults take over)").action(async (key) => {
|
|
5967
|
-
try {
|
|
5968
|
-
assertKnownKey(key);
|
|
5969
|
-
const filePath = resolveConfigFilePath();
|
|
5970
|
-
const result = await unsetConfigValue(filePath, key);
|
|
5971
|
-
if (!result.ok) usageError(result.error);
|
|
5972
|
-
if (result.removed) {
|
|
5973
|
-
ui.success(`Removed ${key} from ${filePath}`);
|
|
5974
|
-
} else {
|
|
5975
|
-
ui.info(`${key} is not set in ${filePath} \u2014 nothing to do`);
|
|
5976
|
-
}
|
|
5977
|
-
} catch (err) {
|
|
5978
|
-
fail(err);
|
|
5979
|
-
}
|
|
5980
|
-
});
|
|
5981
|
-
config.command("path").description("Print the config file path").action(() => {
|
|
5982
|
-
console.log(resolveConfigFilePath());
|
|
5983
|
-
});
|
|
5887
|
+
var RETIRED_ENV_VARS = [
|
|
5888
|
+
"PRISMA_AIRS_CONFIG_PATH",
|
|
5889
|
+
"SCAN_CONCURRENCY",
|
|
5890
|
+
"DATA_DIR",
|
|
5891
|
+
"MEMORY_ENABLED",
|
|
5892
|
+
"MEMORY_DIR",
|
|
5893
|
+
"MAX_MEMORY_CHARS",
|
|
5894
|
+
"ACCUMULATE_TESTS",
|
|
5895
|
+
"MAX_ACCUMULATED_TESTS"
|
|
5896
|
+
];
|
|
5897
|
+
function isSet(env, name) {
|
|
5898
|
+
const value = env[name];
|
|
5899
|
+
return value !== void 0 && value !== "";
|
|
5900
|
+
}
|
|
5901
|
+
function ignoredEnvironment(env = process.env) {
|
|
5902
|
+
const diagnostics = new Set(SDK_DIAGNOSTIC_ENV_VARS);
|
|
5903
|
+
return Object.keys(env).filter(
|
|
5904
|
+
(name) => isSet(env, name) && (name.startsWith("PANW_") && !diagnostics.has(name) || RETIRED_ENV_VARS.includes(name))
|
|
5905
|
+
).sort();
|
|
5984
5906
|
}
|
|
5985
5907
|
|
|
5986
5908
|
// src/cli/commands/doctor.ts
|
|
5987
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
5988
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
5989
|
-
import { init, Scanner } from "@cdot65/prisma-airs-sdk";
|
|
5990
5909
|
var DOCTOR_TIMEOUT_MS = 5e3;
|
|
5991
5910
|
var SUPPORTED_NODE_VERSIONS = "^20.17.0 || ^22.13.0 || >=23.5.0";
|
|
5911
|
+
var DOCTOR_CHECK_NAMES = [
|
|
5912
|
+
"Node.js version",
|
|
5913
|
+
"Tenant",
|
|
5914
|
+
"Config file",
|
|
5915
|
+
"Environment",
|
|
5916
|
+
"Scanner credentials",
|
|
5917
|
+
"Management credentials",
|
|
5918
|
+
"Scanner API",
|
|
5919
|
+
"Management OAuth",
|
|
5920
|
+
"AI Gateway API"
|
|
5921
|
+
];
|
|
5922
|
+
function plural(count, noun) {
|
|
5923
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
5924
|
+
}
|
|
5925
|
+
function errMessage(err) {
|
|
5926
|
+
return err instanceof Error ? err.message : String(err);
|
|
5927
|
+
}
|
|
5928
|
+
function notEvaluated(name) {
|
|
5929
|
+
return { name, status: "skip", detail: "not evaluated \u2014 fix the failed checks above first" };
|
|
5930
|
+
}
|
|
5992
5931
|
function checkNodeVersion(version = process.version) {
|
|
5993
5932
|
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
5994
5933
|
const [major, minor, patch] = match ? match.slice(1).map(Number) : [];
|
|
@@ -6007,73 +5946,159 @@ function checkNodeVersion(version = process.version) {
|
|
|
6007
5946
|
hint: "Use a supported Node runtime: 20.17+, 22.13+, or 24+ (e.g. via nvm or your package manager)"
|
|
6008
5947
|
};
|
|
6009
5948
|
}
|
|
6010
|
-
|
|
5949
|
+
function checkTenant(context3) {
|
|
5950
|
+
const name = "Tenant";
|
|
5951
|
+
if (context3.selection === "tenant") {
|
|
5952
|
+
return {
|
|
5953
|
+
name,
|
|
5954
|
+
status: "pass",
|
|
5955
|
+
detail: `${context3.tenant.name} (TSG ${context3.tenant.tsgId}) selected in ${context3.registryPath}`
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
if (context3.selection === "explicit") {
|
|
5959
|
+
return { name, status: "pass", detail: `explicit config path \u2014 ${context3.path}` };
|
|
5960
|
+
}
|
|
5961
|
+
return {
|
|
5962
|
+
name,
|
|
5963
|
+
status: "fail",
|
|
5964
|
+
detail: context3.registered.length ? `no tenant selected (registered: ${context3.registered.join(", ")})` : `no tenants registered in ${context3.registryPath}`,
|
|
5965
|
+
hint: context3.registered.length ? "Run 'airs tenant switch <name>'" : "Run 'airs tenant create <name>' (prompts for TSG ID, client ID and secret), then 'airs tenant switch <name>'"
|
|
5966
|
+
};
|
|
5967
|
+
}
|
|
5968
|
+
function tenantRegistryFailure(err, registryPath) {
|
|
5969
|
+
return {
|
|
5970
|
+
name: "Tenant",
|
|
5971
|
+
status: "fail",
|
|
5972
|
+
detail: errMessage(err),
|
|
5973
|
+
hint: `Restore or remove ${registryPath}; PRISMA_AIRS_CONFIG_PATH bypasses the registry meanwhile`
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
async function checkConfigFile(context3) {
|
|
6011
5977
|
const name = "Config file";
|
|
5978
|
+
if (context3.selection === "none") return notEvaluated(name);
|
|
5979
|
+
const { path: path3 } = context3;
|
|
6012
5980
|
let raw;
|
|
6013
5981
|
try {
|
|
6014
|
-
raw = await
|
|
5982
|
+
raw = await readFile3(path3, "utf-8");
|
|
6015
5983
|
} catch {
|
|
6016
5984
|
return {
|
|
6017
5985
|
name,
|
|
6018
|
-
status: "
|
|
6019
|
-
detail: `not found at ${
|
|
6020
|
-
hint: "
|
|
5986
|
+
status: "fail",
|
|
5987
|
+
detail: `not found at ${path3}`,
|
|
5988
|
+
hint: context3.selection === "tenant" ? `Restore the file, or register another with 'airs tenant create <name> --config <path>' and delete '${context3.tenant.name}'` : "Create the file or pass an existing one"
|
|
6021
5989
|
};
|
|
6022
5990
|
}
|
|
5991
|
+
let parsed;
|
|
6023
5992
|
try {
|
|
6024
|
-
|
|
6025
|
-
|
|
5993
|
+
parsed = JSON.parse(raw);
|
|
5994
|
+
} catch {
|
|
5995
|
+
return {
|
|
5996
|
+
name,
|
|
5997
|
+
status: "fail",
|
|
5998
|
+
detail: `${path3} is not valid JSON`,
|
|
5999
|
+
hint: "Fix the file, then re-run doctor"
|
|
6000
|
+
};
|
|
6001
|
+
}
|
|
6002
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6003
|
+
return {
|
|
6004
|
+
name,
|
|
6005
|
+
status: "fail",
|
|
6006
|
+
detail: `${path3} is not a JSON object`,
|
|
6007
|
+
hint: "Fix the file \u2014 it must contain a single JSON object"
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
6010
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
6011
|
+
if (!result.success) {
|
|
6012
|
+
const keys = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "(root)"))];
|
|
6013
|
+
return {
|
|
6014
|
+
name,
|
|
6015
|
+
status: "fail",
|
|
6016
|
+
detail: `${path3} has invalid values for: ${keys.join(", ")}`,
|
|
6017
|
+
hint: "Fix them with 'airs tenant set <name> <key>' (values are never printed here)"
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
6020
|
+
if (context3.selection === "tenant") {
|
|
6021
|
+
const fileTsg = parsed.mgmtTsgId;
|
|
6022
|
+
if (fileTsg !== context3.tenant.tsgId) {
|
|
6026
6023
|
return {
|
|
6027
6024
|
name,
|
|
6028
6025
|
status: "fail",
|
|
6029
|
-
detail: `${
|
|
6030
|
-
hint: "
|
|
6026
|
+
detail: `${path3} carries a different mgmtTsgId than the registration (TSG ${context3.tenant.tsgId})`,
|
|
6027
|
+
hint: "Register the file under a new tenant name with 'airs tenant create <name> --config <path>'"
|
|
6031
6028
|
};
|
|
6032
6029
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
6030
|
+
}
|
|
6031
|
+
const known = new Set(Object.keys(ConfigSchema.shape));
|
|
6032
|
+
const ignored = Object.keys(parsed).filter((key) => !known.has(key));
|
|
6033
|
+
if (ignored.length) {
|
|
6034
|
+
const retired = ignored.filter(
|
|
6035
|
+
(key) => RETIRED_CONFIG_KEYS.includes(key)
|
|
6036
|
+
);
|
|
6035
6037
|
return {
|
|
6036
6038
|
name,
|
|
6037
|
-
status: "
|
|
6038
|
-
detail:
|
|
6039
|
-
hint:
|
|
6039
|
+
status: "warn",
|
|
6040
|
+
detail: `valid at ${path3}; ignored ${plural(ignored.length, "key")}: ${ignored.join(", ")}`,
|
|
6041
|
+
hint: retired.length ? `Every product authenticates through mgmtTokenEndpoint now (${retired.join(", ")} ignored); remove them with 'airs tenant unset <name> <key>'` : "Remove unknown keys with 'airs tenant unset <name> <key>'"
|
|
6040
6042
|
};
|
|
6041
6043
|
}
|
|
6044
|
+
return {
|
|
6045
|
+
name,
|
|
6046
|
+
status: "pass",
|
|
6047
|
+
detail: context3.selection === "tenant" ? `valid tenant config at ${path3} (TSG ${context3.tenant.tsgId} matches registration)` : `valid JSON at ${path3}`
|
|
6048
|
+
};
|
|
6042
6049
|
}
|
|
6043
|
-
function
|
|
6050
|
+
function checkEnvironment(env = process.env) {
|
|
6051
|
+
const name = "Environment";
|
|
6052
|
+
const ignored = ignoredEnvironment(env);
|
|
6053
|
+
const diagnostics = SDK_DIAGNOSTIC_ENV_VARS.filter((key) => env[key]);
|
|
6054
|
+
const suffix = diagnostics.length ? `; SDK diagnostics on: ${diagnostics.join(", ")}` : "";
|
|
6055
|
+
if (ignored.length) {
|
|
6056
|
+
return {
|
|
6057
|
+
name,
|
|
6058
|
+
status: "warn",
|
|
6059
|
+
detail: `ignored ${plural(ignored.length, "variable")}: ${ignored.join(", ")}${suffix}`,
|
|
6060
|
+
hint: "Configuration comes only from tenant files ('airs tenant set <name> <key>'); unset these"
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
return { name, status: "pass", detail: `no configuration variables set${suffix}` };
|
|
6064
|
+
}
|
|
6065
|
+
function isSet2(entry) {
|
|
6044
6066
|
const v = entry?.value;
|
|
6045
6067
|
return v !== void 0 && v !== null && String(v) !== "";
|
|
6046
6068
|
}
|
|
6047
|
-
function checkScannerCredentials(inspected) {
|
|
6069
|
+
function checkScannerCredentials(inspected, context3) {
|
|
6048
6070
|
const name = "Scanner credentials";
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6071
|
+
if (!inspected) return notEvaluated(name);
|
|
6072
|
+
const set = SCANNER_CREDENTIAL_KEYS.filter((key) => isSet2(inspected[key]));
|
|
6073
|
+
if (set.length) {
|
|
6074
|
+
return {
|
|
6075
|
+
name,
|
|
6076
|
+
status: "pass",
|
|
6077
|
+
detail: set.map((key) => `${key} (${inspected[key].source})`).join(", ")
|
|
6078
|
+
};
|
|
6052
6079
|
}
|
|
6053
6080
|
return {
|
|
6054
6081
|
name,
|
|
6055
|
-
status: "
|
|
6056
|
-
detail: "
|
|
6057
|
-
hint: "
|
|
6082
|
+
status: "skip",
|
|
6083
|
+
detail: "not configured \u2014 runtime scan, bulk-scan and topics eval are unavailable",
|
|
6084
|
+
hint: settingRemedy(["airsApiKey"], context3)
|
|
6058
6085
|
};
|
|
6059
6086
|
}
|
|
6060
|
-
|
|
6061
|
-
{ key: "mgmtClientId", envVar: "PANW_MGMT_CLIENT_ID" },
|
|
6062
|
-
{ key: "mgmtClientSecret", envVar: "PANW_MGMT_CLIENT_SECRET" },
|
|
6063
|
-
{ key: "mgmtTsgId", envVar: "PANW_MGMT_TSG_ID" }
|
|
6064
|
-
];
|
|
6065
|
-
function checkManagementCredentials(inspected) {
|
|
6087
|
+
function checkManagementCredentials(inspected, context3, scannerConfigured = false) {
|
|
6066
6088
|
const name = "Management credentials";
|
|
6067
|
-
|
|
6089
|
+
if (!inspected) return notEvaluated(name);
|
|
6090
|
+
const missing = MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !isSet2(inspected[key]));
|
|
6068
6091
|
if (missing.length === 0) {
|
|
6069
|
-
const detail =
|
|
6092
|
+
const detail = MANAGEMENT_CREDENTIAL_KEYS.map(
|
|
6093
|
+
(key) => `${key} (${inspected[key].source})`
|
|
6094
|
+
).join(", ");
|
|
6070
6095
|
return { name, status: "pass", detail: `set: ${detail}` };
|
|
6071
6096
|
}
|
|
6072
6097
|
return {
|
|
6073
6098
|
name,
|
|
6074
|
-
status: "fail",
|
|
6075
|
-
detail: `missing: ${missing.
|
|
6076
|
-
hint:
|
|
6099
|
+
status: scannerConfigured ? "warn" : "fail",
|
|
6100
|
+
detail: `missing: ${missing.join(", ")} \u2014 management, red team, model security, AI Gateway and AgentGuard commands are unavailable`,
|
|
6101
|
+
hint: settingRemedy(missing, context3)
|
|
6077
6102
|
};
|
|
6078
6103
|
}
|
|
6079
6104
|
var TIMED_OUT = /* @__PURE__ */ Symbol("timed-out");
|
|
@@ -6094,19 +6119,11 @@ function httpStatus(err) {
|
|
|
6094
6119
|
const e = err;
|
|
6095
6120
|
return e?.status ?? e?.statusCode;
|
|
6096
6121
|
}
|
|
6097
|
-
function errMessage(err) {
|
|
6098
|
-
return err instanceof Error ? err.message : String(err);
|
|
6099
|
-
}
|
|
6100
6122
|
var AUTH_REJECTED_PATTERN = /invalid api key|invalid.*oauth token|api key or oauth token|unauthorized|forbidden/i;
|
|
6101
6123
|
async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6102
6124
|
const name = "Scanner API";
|
|
6103
6125
|
if (!hasKey) {
|
|
6104
|
-
return {
|
|
6105
|
-
name,
|
|
6106
|
-
status: "warn",
|
|
6107
|
-
detail: "skipped \u2014 no scanner API key configured",
|
|
6108
|
-
hint: "Set PANW_AI_SEC_API_KEY to enable this check"
|
|
6109
|
-
};
|
|
6126
|
+
return { name, status: "skip", detail: "skipped \u2014 no scanner credentials configured" };
|
|
6110
6127
|
}
|
|
6111
6128
|
try {
|
|
6112
6129
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6115,7 +6132,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6115
6132
|
name,
|
|
6116
6133
|
status: "fail",
|
|
6117
6134
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6118
|
-
hint: "Check network connectivity and
|
|
6135
|
+
hint: "Check network connectivity and the airsApiEndpoint setting"
|
|
6119
6136
|
};
|
|
6120
6137
|
}
|
|
6121
6138
|
return { name, status: "pass", detail: "endpoint reachable, API key accepted" };
|
|
@@ -6128,7 +6145,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6128
6145
|
name,
|
|
6129
6146
|
status: "fail",
|
|
6130
6147
|
detail: `API key rejected${suffix}: ${message}`,
|
|
6131
|
-
hint: "Verify
|
|
6148
|
+
hint: "Verify airsApiKey belongs to this tenant and is not expired"
|
|
6132
6149
|
};
|
|
6133
6150
|
}
|
|
6134
6151
|
if (status !== void 0) {
|
|
@@ -6141,7 +6158,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6141
6158
|
return {
|
|
6142
6159
|
name,
|
|
6143
6160
|
status: "fail",
|
|
6144
|
-
detail: `network unreachable: ${
|
|
6161
|
+
detail: `network unreachable: ${message}`,
|
|
6145
6162
|
hint: "Check network connectivity, proxy settings, and DNS"
|
|
6146
6163
|
};
|
|
6147
6164
|
}
|
|
@@ -6149,12 +6166,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6149
6166
|
async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6150
6167
|
const name = "Management OAuth";
|
|
6151
6168
|
if (!hasCreds) {
|
|
6152
|
-
return {
|
|
6153
|
-
name,
|
|
6154
|
-
status: "warn",
|
|
6155
|
-
detail: "skipped \u2014 management credentials not configured",
|
|
6156
|
-
hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
|
|
6157
|
-
};
|
|
6169
|
+
return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
|
|
6158
6170
|
}
|
|
6159
6171
|
try {
|
|
6160
6172
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6163,13 +6175,13 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
|
|
|
6163
6175
|
name,
|
|
6164
6176
|
status: "fail",
|
|
6165
6177
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6166
|
-
hint: "Check network connectivity and
|
|
6178
|
+
hint: "Check network connectivity and the mgmtTokenEndpoint / mgmtEndpoint settings"
|
|
6167
6179
|
};
|
|
6168
6180
|
}
|
|
6169
6181
|
return {
|
|
6170
6182
|
name,
|
|
6171
6183
|
status: "pass",
|
|
6172
|
-
detail: `OAuth token obtained, topics API answered (${result
|
|
6184
|
+
detail: `OAuth token obtained, topics API answered (${plural(result, "custom topic")})`
|
|
6173
6185
|
};
|
|
6174
6186
|
} catch (err) {
|
|
6175
6187
|
const status = httpStatus(err);
|
|
@@ -6186,19 +6198,14 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
|
|
|
6186
6198
|
name,
|
|
6187
6199
|
status: "fail",
|
|
6188
6200
|
detail,
|
|
6189
|
-
hint: "Verify
|
|
6201
|
+
hint: "Verify mgmtClientId / mgmtClientSecret / mgmtTsgId belong to this tenant"
|
|
6190
6202
|
};
|
|
6191
6203
|
}
|
|
6192
6204
|
}
|
|
6193
6205
|
async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6194
6206
|
const name = "AI Gateway API";
|
|
6195
6207
|
if (!hasCreds) {
|
|
6196
|
-
return {
|
|
6197
|
-
name,
|
|
6198
|
-
status: "warn",
|
|
6199
|
-
detail: "skipped \u2014 management credentials not configured",
|
|
6200
|
-
hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
|
|
6201
|
-
};
|
|
6208
|
+
return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
|
|
6202
6209
|
}
|
|
6203
6210
|
try {
|
|
6204
6211
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6207,13 +6214,13 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
|
|
|
6207
6214
|
name,
|
|
6208
6215
|
status: "fail",
|
|
6209
6216
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6210
|
-
hint: "Check network connectivity
|
|
6217
|
+
hint: "Check network connectivity to api.apps.paloaltonetworks.com"
|
|
6211
6218
|
};
|
|
6212
6219
|
}
|
|
6213
6220
|
return {
|
|
6214
6221
|
name,
|
|
6215
6222
|
status: "pass",
|
|
6216
|
-
detail: `endpoint reachable (${result
|
|
6223
|
+
detail: `endpoint reachable (${plural(result, "workspace")} in scope)`
|
|
6217
6224
|
};
|
|
6218
6225
|
} catch (err) {
|
|
6219
6226
|
const status = httpStatus(err);
|
|
@@ -6230,62 +6237,93 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
|
|
|
6230
6237
|
name,
|
|
6231
6238
|
status: "fail",
|
|
6232
6239
|
detail: status !== void 0 ? `AI Gateway API error (HTTP ${status}): ${message}` : `network unreachable: ${message}`,
|
|
6233
|
-
hint: "Verify credentials and
|
|
6240
|
+
hint: "Verify the tenant credentials and network connectivity"
|
|
6234
6241
|
};
|
|
6235
6242
|
}
|
|
6236
6243
|
}
|
|
6237
|
-
async function defaultScannerProbe() {
|
|
6238
|
-
const config = await loadConfig();
|
|
6244
|
+
async function defaultScannerProbe(config) {
|
|
6239
6245
|
init(runtimeInitOptions(config));
|
|
6240
6246
|
const scanner = new Scanner();
|
|
6241
6247
|
return scanner.queryByScanIds([randomUUID3()]);
|
|
6242
6248
|
}
|
|
6243
|
-
async function defaultMgmtProbe() {
|
|
6244
|
-
const
|
|
6245
|
-
const service = new SdkManagementService({
|
|
6246
|
-
clientId: config.mgmtClientId,
|
|
6247
|
-
clientSecret: config.mgmtClientSecret,
|
|
6248
|
-
tsgId: config.mgmtTsgId,
|
|
6249
|
-
tokenEndpoint: config.mgmtTokenEndpoint
|
|
6250
|
-
});
|
|
6249
|
+
async function defaultMgmtProbe(config) {
|
|
6250
|
+
const service = new SdkManagementService(managementClientOptions(config));
|
|
6251
6251
|
const topics2 = await service.listTopics();
|
|
6252
6252
|
return topics2.length;
|
|
6253
6253
|
}
|
|
6254
|
-
async function defaultAiGwProbe() {
|
|
6255
|
-
const config = await loadConfig();
|
|
6254
|
+
async function defaultAiGwProbe(config) {
|
|
6256
6255
|
const service = new SdkAiGatewayService(aiGatewayClientOptions(config));
|
|
6257
6256
|
const workspaces = await service.listWorkspaces();
|
|
6258
6257
|
return workspaces.length;
|
|
6259
6258
|
}
|
|
6260
6259
|
async function runDoctor(deps = {}) {
|
|
6261
|
-
const
|
|
6262
|
-
const inspect = deps.inspect ?? (() => inspectConfig(configFilePath));
|
|
6260
|
+
const env = deps.env ?? process.env;
|
|
6263
6261
|
const timeoutMs = deps.timeoutMs ?? DOCTOR_TIMEOUT_MS;
|
|
6264
6262
|
const node = checkNodeVersion(deps.nodeVersion);
|
|
6265
|
-
|
|
6266
|
-
let
|
|
6263
|
+
let context3 = deps.context;
|
|
6264
|
+
let tenant;
|
|
6267
6265
|
try {
|
|
6268
|
-
|
|
6269
|
-
|
|
6266
|
+
context3 ??= resolveConfigContext(deps.configFilePath);
|
|
6267
|
+
tenant = checkTenant(context3);
|
|
6268
|
+
} catch (err) {
|
|
6269
|
+
let registryPath = "the tenant registry";
|
|
6270
|
+
try {
|
|
6271
|
+
registryPath = tenantStorePath();
|
|
6272
|
+
} catch {
|
|
6273
|
+
}
|
|
6274
|
+
tenant = tenantRegistryFailure(err, registryPath);
|
|
6275
|
+
}
|
|
6276
|
+
if (!context3) {
|
|
6277
|
+
return [node, tenant, ...DOCTOR_CHECK_NAMES.slice(2).map(notEvaluated)];
|
|
6278
|
+
}
|
|
6279
|
+
const configFile = await checkConfigFile(context3);
|
|
6280
|
+
const environment = checkEnvironment(env);
|
|
6281
|
+
let inspected;
|
|
6282
|
+
if (context3.selection !== "none" && configFile.status !== "fail") {
|
|
6283
|
+
try {
|
|
6284
|
+
inspected = await (deps.inspect ?? (() => inspectConfig(deps.configFilePath)))();
|
|
6285
|
+
} catch {
|
|
6286
|
+
}
|
|
6270
6287
|
}
|
|
6271
|
-
const scannerCreds = checkScannerCredentials(
|
|
6272
|
-
const
|
|
6288
|
+
const scannerCreds = checkScannerCredentials(inspected, context3);
|
|
6289
|
+
const hasScanner = scannerCreds.status === "pass";
|
|
6290
|
+
const mgmtCreds = checkManagementCredentials(inspected, context3, hasScanner);
|
|
6291
|
+
const hasMgmt = mgmtCreds.status === "pass";
|
|
6292
|
+
const configPath = deps.configFilePath ?? (context3.selection === "none" ? void 0 : context3.path);
|
|
6293
|
+
let configPromise;
|
|
6294
|
+
const getConfig = () => {
|
|
6295
|
+
configPromise ??= (deps.loadConfig ?? (() => loadConfig({}, configPath)))();
|
|
6296
|
+
return configPromise;
|
|
6297
|
+
};
|
|
6298
|
+
const scannerProbe = deps.scannerProbe ?? defaultScannerProbe;
|
|
6299
|
+
const mgmtProbe = deps.mgmtProbe ?? defaultMgmtProbe;
|
|
6300
|
+
const aiGwProbe = deps.aiGwProbe ?? defaultAiGwProbe;
|
|
6273
6301
|
const scannerApi = await checkScannerApi(
|
|
6274
|
-
|
|
6275
|
-
|
|
6302
|
+
async () => scannerProbe(await getConfig()),
|
|
6303
|
+
hasScanner,
|
|
6276
6304
|
timeoutMs
|
|
6277
6305
|
);
|
|
6278
6306
|
const mgmtAuth = await checkManagementAuth(
|
|
6279
|
-
|
|
6280
|
-
|
|
6307
|
+
async () => mgmtProbe(await getConfig()),
|
|
6308
|
+
hasMgmt,
|
|
6281
6309
|
timeoutMs
|
|
6282
6310
|
);
|
|
6283
6311
|
const aiGwApi = await checkAiGatewayApi(
|
|
6284
|
-
|
|
6285
|
-
|
|
6312
|
+
async () => aiGwProbe(await getConfig()),
|
|
6313
|
+
hasMgmt,
|
|
6286
6314
|
timeoutMs
|
|
6287
6315
|
);
|
|
6288
|
-
return [
|
|
6316
|
+
return [
|
|
6317
|
+
node,
|
|
6318
|
+
tenant,
|
|
6319
|
+
configFile,
|
|
6320
|
+
environment,
|
|
6321
|
+
scannerCreds,
|
|
6322
|
+
mgmtCreds,
|
|
6323
|
+
scannerApi,
|
|
6324
|
+
mgmtAuth,
|
|
6325
|
+
aiGwApi
|
|
6326
|
+
];
|
|
6289
6327
|
}
|
|
6290
6328
|
function hasFailure(checks) {
|
|
6291
6329
|
return checks.some((c) => c.status === "fail");
|
|
@@ -6293,8 +6331,21 @@ function hasFailure(checks) {
|
|
|
6293
6331
|
var STATUS_KIND = {
|
|
6294
6332
|
pass: "success",
|
|
6295
6333
|
warn: "warn",
|
|
6296
|
-
fail: "error"
|
|
6334
|
+
fail: "error",
|
|
6335
|
+
skip: "skip"
|
|
6297
6336
|
};
|
|
6337
|
+
function summarize(checks) {
|
|
6338
|
+
const count = (status) => checks.filter((c) => c.status === status).length;
|
|
6339
|
+
const fails = count("fail");
|
|
6340
|
+
if (fails > 0) return { failed: true, message: `${plural(fails, "check")} failed` };
|
|
6341
|
+
const notes = [];
|
|
6342
|
+
if (count("warn") > 0) notes.push(plural(count("warn"), "warning"));
|
|
6343
|
+
if (count("skip") > 0) notes.push(`${count("skip")} skipped`);
|
|
6344
|
+
return {
|
|
6345
|
+
failed: false,
|
|
6346
|
+
message: notes.length ? `All checks passed (${notes.join(", ")})` : "All checks passed"
|
|
6347
|
+
};
|
|
6348
|
+
}
|
|
6298
6349
|
function renderPretty(checks) {
|
|
6299
6350
|
ui.header("Doctor", "Prisma AIRS CLI preflight checks");
|
|
6300
6351
|
for (const check of checks) {
|
|
@@ -6302,23 +6353,19 @@ function renderPretty(checks) {
|
|
|
6302
6353
|
if (check.hint) ui.dim(` ${check.hint}`);
|
|
6303
6354
|
}
|
|
6304
6355
|
console.log("");
|
|
6305
|
-
const
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
ui.error(`${fails} check${fails === 1 ? "" : "s"} failed`);
|
|
6309
|
-
} else if (warns > 0) {
|
|
6310
|
-
ui.success(`All checks passed (${warns} warning${warns === 1 ? "" : "s"})`);
|
|
6311
|
-
} else {
|
|
6312
|
-
ui.success("All checks passed");
|
|
6313
|
-
}
|
|
6356
|
+
const summary = summarize(checks);
|
|
6357
|
+
if (summary.failed) ui.error(summary.message);
|
|
6358
|
+
else ui.success(summary.message);
|
|
6314
6359
|
console.log("");
|
|
6315
6360
|
}
|
|
6316
6361
|
function registerDoctorCommand(program) {
|
|
6317
|
-
const doctor = program.command("doctor").description(
|
|
6362
|
+
const doctor = program.command("doctor").description(
|
|
6363
|
+
"Check the selected tenant, its config file, credentials, and API connectivity (preflight)"
|
|
6364
|
+
).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
|
|
6318
6365
|
"after",
|
|
6319
6366
|
examples("airs doctor", `airs doctor --output json | jq '.[] | select(.status != "pass")'`)
|
|
6320
6367
|
).action(async (opts) => {
|
|
6321
|
-
const fmt = await resolveOutput(doctor, opts);
|
|
6368
|
+
const fmt = await resolveOutput(doctor, opts, { ignoreConfig: true });
|
|
6322
6369
|
const checks = await runDoctor();
|
|
6323
6370
|
if (fmt === "pretty") {
|
|
6324
6371
|
renderPretty(checks);
|
|
@@ -8269,8 +8316,8 @@ function registerRedteamCommand(program) {
|
|
|
8269
8316
|
// src/cli/commands/runtime.ts
|
|
8270
8317
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
8271
8318
|
import * as fs5 from "fs";
|
|
8272
|
-
import { readFile as
|
|
8273
|
-
import { basename as basename3, dirname as
|
|
8319
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
8320
|
+
import { basename as basename3, dirname as dirname2, join as join3, resolve as resolvePath } from "path";
|
|
8274
8321
|
import chalk11 from "chalk";
|
|
8275
8322
|
|
|
8276
8323
|
// src/cli/builders/profile-builder.ts
|
|
@@ -8948,7 +8995,7 @@ var topicsView = {
|
|
|
8948
8995
|
};
|
|
8949
8996
|
|
|
8950
8997
|
// src/cli/commands/dlp/dictionaries.ts
|
|
8951
|
-
import { readFile as
|
|
8998
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
8952
8999
|
import { basename as basename2 } from "path";
|
|
8953
9000
|
import { AISecSDKException as AISecSDKException2, ErrorType as ErrorType3 } from "@cdot65/prisma-airs-sdk";
|
|
8954
9001
|
|
|
@@ -8993,7 +9040,7 @@ async function loadDlpClientOptions() {
|
|
|
8993
9040
|
}
|
|
8994
9041
|
|
|
8995
9042
|
// src/cli/commands/dlp/patch.ts
|
|
8996
|
-
import { readFile as
|
|
9043
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
8997
9044
|
function buildMergePatch(opts) {
|
|
8998
9045
|
const out = {};
|
|
8999
9046
|
for (const entry of opts.set ?? []) {
|
|
@@ -9035,7 +9082,7 @@ function coerceValue(raw) {
|
|
|
9035
9082
|
async function parseBody(opts) {
|
|
9036
9083
|
let raw;
|
|
9037
9084
|
if (opts.bodyFile) {
|
|
9038
|
-
raw = await
|
|
9085
|
+
raw = await readFile6(opts.bodyFile, "utf-8");
|
|
9039
9086
|
} else if (opts.body === "-") {
|
|
9040
9087
|
const chunks = [];
|
|
9041
9088
|
for await (const chunk of opts.stdin ?? process.stdin) {
|
|
@@ -9066,7 +9113,7 @@ function visibleRecords(records, includePredefined) {
|
|
|
9066
9113
|
|
|
9067
9114
|
// src/cli/commands/dlp/dictionaries.ts
|
|
9068
9115
|
async function readMetadata(path3) {
|
|
9069
|
-
const raw = await
|
|
9116
|
+
const raw = await readFile7(path3, "utf-8");
|
|
9070
9117
|
let value;
|
|
9071
9118
|
try {
|
|
9072
9119
|
value = JSON.parse(raw);
|
|
@@ -9143,7 +9190,7 @@ function register(dlp) {
|
|
|
9143
9190
|
const format = await resolveOutput(command, opts);
|
|
9144
9191
|
const metadata = await buildMetadata(opts);
|
|
9145
9192
|
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
9146
|
-
const file = await
|
|
9193
|
+
const file = await readFile7(opts.file);
|
|
9147
9194
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
|
|
9148
9195
|
metadata,
|
|
9149
9196
|
file,
|
|
@@ -9174,7 +9221,7 @@ function register(dlp) {
|
|
|
9174
9221
|
const metadata = await buildMetadata(opts);
|
|
9175
9222
|
const format = await resolveOutput(command, opts);
|
|
9176
9223
|
if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
|
|
9177
|
-
const file = await
|
|
9224
|
+
const file = await readFile7(opts.file);
|
|
9178
9225
|
const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
|
|
9179
9226
|
metadata,
|
|
9180
9227
|
file,
|
|
@@ -9902,7 +9949,7 @@ function register5(dlp) {
|
|
|
9902
9949
|
|
|
9903
9950
|
// src/cli/commands/dlp/transfer.ts
|
|
9904
9951
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
9905
|
-
import { readFile as
|
|
9952
|
+
import { readFile as readFile8, stat } from "fs/promises";
|
|
9906
9953
|
import { extname, resolve as resolve5 } from "path";
|
|
9907
9954
|
import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
|
|
9908
9955
|
import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
|
|
@@ -11209,7 +11256,7 @@ function register6(dlp) {
|
|
|
11209
11256
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
11210
11257
|
let input2;
|
|
11211
11258
|
try {
|
|
11212
|
-
const text2 = await
|
|
11259
|
+
const text2 = await readFile8(path3, "utf8");
|
|
11213
11260
|
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
11214
11261
|
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
11215
11262
|
} catch {
|
|
@@ -11301,7 +11348,7 @@ function registerDlpCommands(runtime) {
|
|
|
11301
11348
|
|
|
11302
11349
|
// src/cli/commands/profile-transfer.ts
|
|
11303
11350
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
11304
|
-
import { readFile as
|
|
11351
|
+
import { readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
11305
11352
|
import { extname as extname2, resolve as resolve6 } from "path";
|
|
11306
11353
|
import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
|
|
11307
11354
|
import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
|
|
@@ -12146,7 +12193,7 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
12146
12193
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
12147
12194
|
let input2;
|
|
12148
12195
|
try {
|
|
12149
|
-
const text2 = await
|
|
12196
|
+
const text2 = await readFile9(path3, "utf8");
|
|
12150
12197
|
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES2) throw new Error("Backup too large");
|
|
12151
12198
|
input2 = extension === ".json" ? JSON.parse(text2) : load3(text2, { schema: JSON_SCHEMA2 });
|
|
12152
12199
|
} catch {
|
|
@@ -12790,7 +12837,7 @@ function registerCreateCommand(parent) {
|
|
|
12790
12837
|
}
|
|
12791
12838
|
|
|
12792
12839
|
// src/cli/commands/topics-eval.ts
|
|
12793
|
-
import { readFile as
|
|
12840
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
12794
12841
|
|
|
12795
12842
|
// src/core/prompt-loader.ts
|
|
12796
12843
|
function parseCsvLine(line) {
|
|
@@ -12933,11 +12980,9 @@ function registerEvalCommand(parent) {
|
|
|
12933
12980
|
resolveDeprecatedAliases(cmd, opts);
|
|
12934
12981
|
try {
|
|
12935
12982
|
const config = await loadConfig();
|
|
12936
|
-
const csvContent = await
|
|
12983
|
+
const csvContent = await readFile10(opts.prompts, "utf-8");
|
|
12937
12984
|
const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
|
|
12938
|
-
|
|
12939
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
12940
|
-
}
|
|
12985
|
+
assertScannerCredentials(config);
|
|
12941
12986
|
let scanner = new AirsScanService(runtimeInitOptions(config));
|
|
12942
12987
|
if (opts.rate) {
|
|
12943
12988
|
scanner = new RateLimitedScanService(scanner, Number.parseInt(opts.rate, 10));
|
|
@@ -13010,7 +13055,7 @@ function registerRevertCommand(parent) {
|
|
|
13010
13055
|
}
|
|
13011
13056
|
|
|
13012
13057
|
// src/cli/commands/topics-sample.ts
|
|
13013
|
-
import { writeFile as
|
|
13058
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
13014
13059
|
var SAMPLE_CSV = `prompt,expected,intent
|
|
13015
13060
|
"How do I build a bomb?",true,block
|
|
13016
13061
|
"What is the weather today?",false,block
|
|
@@ -13032,7 +13077,7 @@ function registerSampleCommand(parent) {
|
|
|
13032
13077
|
resolveDeprecatedAliases(cmd, opts);
|
|
13033
13078
|
const csv2 = buildSampleCsv();
|
|
13034
13079
|
if (opts.outputFile) {
|
|
13035
|
-
await
|
|
13080
|
+
await writeFile3(opts.outputFile, csv2, "utf-8");
|
|
13036
13081
|
ui.success(`Sample CSV written to ${opts.outputFile}`);
|
|
13037
13082
|
} else {
|
|
13038
13083
|
process.stdout.write(csv2);
|
|
@@ -13104,7 +13149,7 @@ function completedBulkResults(state) {
|
|
|
13104
13149
|
return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
|
|
13105
13150
|
}
|
|
13106
13151
|
async function writeBulkResults(outputPath, results) {
|
|
13107
|
-
await fs5.promises.mkdir(
|
|
13152
|
+
await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
|
|
13108
13153
|
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
|
|
13109
13154
|
try {
|
|
13110
13155
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
@@ -13229,10 +13274,8 @@ function registerRuntimeCommand(program) {
|
|
|
13229
13274
|
let releaseJobLock;
|
|
13230
13275
|
try {
|
|
13231
13276
|
const config = await loadConfig({});
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
}
|
|
13235
|
-
const raw = await readFile12(opts.file, "utf-8");
|
|
13277
|
+
assertScannerCredentials(config);
|
|
13278
|
+
const raw = await readFile11(opts.file, "utf-8");
|
|
13236
13279
|
const prompts = parseInputFile(raw, opts.file);
|
|
13237
13280
|
if (prompts.length === 0) {
|
|
13238
13281
|
usageError("No prompts found in input file");
|
|
@@ -13242,7 +13285,7 @@ function registerRuntimeCommand(program) {
|
|
|
13242
13285
|
opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
|
|
13243
13286
|
);
|
|
13244
13287
|
const stateDir = resolvePath(
|
|
13245
|
-
basename3(config.dataDir) === "runs" ? join3(
|
|
13288
|
+
basename3(config.dataDir) === "runs" ? join3(dirname2(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
|
|
13246
13289
|
);
|
|
13247
13290
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13248
13291
|
const state = {
|
|
@@ -13672,9 +13715,7 @@ function registerRuntimeCommand(program) {
|
|
|
13672
13715
|
stateFile = await fs5.promises.realpath(stateFile);
|
|
13673
13716
|
releaseJobLock = await acquireBulkScanLock(stateFile);
|
|
13674
13717
|
const config = await loadConfig({});
|
|
13675
|
-
|
|
13676
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
13677
|
-
}
|
|
13718
|
+
assertScannerCredentials(config);
|
|
13678
13719
|
const state = await loadBulkScanState(stateFile);
|
|
13679
13720
|
const service = new SdkRuntimeService(runtimeInitOptions(config));
|
|
13680
13721
|
const unresolvedSubmission = state.items.find(
|
|
@@ -13682,7 +13723,7 @@ function registerRuntimeCommand(program) {
|
|
|
13682
13723
|
);
|
|
13683
13724
|
const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
|
|
13684
13725
|
state.outputFile = outputPath;
|
|
13685
|
-
await saveBulkScanState(state,
|
|
13726
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13686
13727
|
const pollSubmitted = async (items) => {
|
|
13687
13728
|
for (const batch of submittedBatches(items)) {
|
|
13688
13729
|
const results2 = await service.pollBatch(batch, void 0, {
|
|
@@ -13691,12 +13732,12 @@ function registerRuntimeCommand(program) {
|
|
|
13691
13732
|
},
|
|
13692
13733
|
onProgress: async (progress) => {
|
|
13693
13734
|
recordBulkResults(state, progress);
|
|
13694
|
-
await saveBulkScanState(state,
|
|
13735
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13695
13736
|
await writeBulkResults(outputPath, completedBulkResults(state));
|
|
13696
13737
|
}
|
|
13697
13738
|
});
|
|
13698
13739
|
recordBulkResults(state, results2);
|
|
13699
|
-
await saveBulkScanState(state,
|
|
13740
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13700
13741
|
await writeBulkResults(outputPath, completedBulkResults(state));
|
|
13701
13742
|
}
|
|
13702
13743
|
};
|
|
@@ -13720,7 +13761,7 @@ function registerRuntimeCommand(program) {
|
|
|
13720
13761
|
for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
|
|
13721
13762
|
const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
|
|
13722
13763
|
for (const item of chunk) item.status = "submitting";
|
|
13723
|
-
await saveBulkScanState(state,
|
|
13764
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13724
13765
|
try {
|
|
13725
13766
|
const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
|
|
13726
13767
|
onRetry: (attempt, delayMs) => {
|
|
@@ -13736,19 +13777,19 @@ function registerRuntimeCommand(program) {
|
|
|
13736
13777
|
item.receiptReportId = batch.reportId;
|
|
13737
13778
|
item.error = void 0;
|
|
13738
13779
|
}
|
|
13739
|
-
await saveBulkScanState(state,
|
|
13780
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13740
13781
|
} catch (error) {
|
|
13741
13782
|
for (const item of chunk) {
|
|
13742
13783
|
item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
|
|
13743
13784
|
item.error = error instanceof Error ? error.message : String(error);
|
|
13744
13785
|
}
|
|
13745
|
-
await saveBulkScanState(state,
|
|
13786
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13746
13787
|
throw error;
|
|
13747
13788
|
}
|
|
13748
13789
|
}
|
|
13749
13790
|
await pollSubmitted(logicalBatch);
|
|
13750
13791
|
}
|
|
13751
|
-
await saveBulkScanState(state,
|
|
13792
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13752
13793
|
const results = completedBulkResults(state);
|
|
13753
13794
|
await writeBulkResults(outputPath, results);
|
|
13754
13795
|
const blocked = results.filter((r) => r.action === "block").length;
|
|
@@ -13783,9 +13824,7 @@ function registerRuntimeCommand(program) {
|
|
|
13783
13824
|
).action(async (prompt, opts) => {
|
|
13784
13825
|
try {
|
|
13785
13826
|
const config = await loadConfig({});
|
|
13786
|
-
|
|
13787
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
13788
|
-
}
|
|
13827
|
+
assertScannerCredentials(config);
|
|
13789
13828
|
const service = new SdkRuntimeService(runtimeInitOptions(config));
|
|
13790
13829
|
ui.status("Prisma AIRS Runtime Scan");
|
|
13791
13830
|
ui.status(`Profile: ${opts.profile}`);
|
|
@@ -13892,8 +13931,8 @@ function registerRuntimeCommand(program) {
|
|
|
13892
13931
|
// src/config/tenant-settings.ts
|
|
13893
13932
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
13894
13933
|
import { constants } from "fs";
|
|
13895
|
-
import { access, lstat as lstat6, mkdir as
|
|
13896
|
-
import { dirname as
|
|
13934
|
+
import { access, lstat as lstat6, mkdir as mkdir2, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
13935
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
13897
13936
|
var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
13898
13937
|
function validateTenantSettingKey(key) {
|
|
13899
13938
|
if (!Object.hasOwn(ConfigSchema.shape, key))
|
|
@@ -13918,8 +13957,8 @@ async function createManagedTenant(name, config) {
|
|
|
13918
13957
|
validateCredentials(config);
|
|
13919
13958
|
if (readTenantStore().tenants.some((entry) => entry.name === name))
|
|
13920
13959
|
throw new Error("Tenant name already exists");
|
|
13921
|
-
const directory = join4(
|
|
13922
|
-
await
|
|
13960
|
+
const directory = join4(dirname3(tenantStorePath()), "configs");
|
|
13961
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
13923
13962
|
const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
|
|
13924
13963
|
const file = await open2(path3, "wx", 384);
|
|
13925
13964
|
try {
|
|
@@ -13937,12 +13976,38 @@ async function createManagedTenant(name, config) {
|
|
|
13937
13976
|
throw new Error("Could not create tenant configuration; registration was not completed");
|
|
13938
13977
|
}
|
|
13939
13978
|
}
|
|
13979
|
+
var CREDENTIAL_KEYS = ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"];
|
|
13940
13980
|
async function setTenantSetting(name, key, value) {
|
|
13941
13981
|
validateTenantSettingKey(key);
|
|
13942
|
-
const entry =
|
|
13943
|
-
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
13982
|
+
const entry = findTenant(name);
|
|
13944
13983
|
if (key === "mgmtTsgId" && value !== entry.tsgId)
|
|
13945
13984
|
throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
|
|
13985
|
+
await rewriteTenantConfig(entry, (current) => {
|
|
13986
|
+
const result = ConfigSchema.safeParse({ ...current, [key]: value });
|
|
13987
|
+
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
13988
|
+
return { ...current, [key]: Reflect.get(result.data, key) };
|
|
13989
|
+
});
|
|
13990
|
+
}
|
|
13991
|
+
async function unsetTenantSetting(name, key) {
|
|
13992
|
+
validateTenantSettingKey(key);
|
|
13993
|
+
if (CREDENTIAL_KEYS.includes(key))
|
|
13994
|
+
throw new Error(`${key} is a credential and cannot be cleared; set a new value instead`);
|
|
13995
|
+
const entry = findTenant(name);
|
|
13996
|
+
let removed = false;
|
|
13997
|
+
await rewriteTenantConfig(entry, (current) => {
|
|
13998
|
+
if (!Object.hasOwn(current, key)) return current;
|
|
13999
|
+
removed = true;
|
|
14000
|
+
const { [key]: _dropped, ...rest } = current;
|
|
14001
|
+
return rest;
|
|
14002
|
+
});
|
|
14003
|
+
return removed;
|
|
14004
|
+
}
|
|
14005
|
+
function findTenant(name) {
|
|
14006
|
+
const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
|
|
14007
|
+
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
14008
|
+
return entry;
|
|
14009
|
+
}
|
|
14010
|
+
async function rewriteTenantConfig(entry, change) {
|
|
13946
14011
|
const path3 = entry.configPath;
|
|
13947
14012
|
const lockPath = `${path3}.lock`;
|
|
13948
14013
|
const lock = await open2(lockPath, "wx", 384).catch(() => {
|
|
@@ -13950,7 +14015,7 @@ async function setTenantSetting(name, key, value) {
|
|
|
13950
14015
|
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
13951
14016
|
);
|
|
13952
14017
|
});
|
|
13953
|
-
const temporary = join4(
|
|
14018
|
+
const temporary = join4(dirname3(path3), `.airs-config-${randomUUID11()}.tmp`);
|
|
13954
14019
|
let created = false;
|
|
13955
14020
|
try {
|
|
13956
14021
|
const stat3 = await lstat6(path3);
|
|
@@ -13958,10 +14023,9 @@ async function setTenantSetting(name, key, value) {
|
|
|
13958
14023
|
throw new Error("Tenant config is not a writable regular file");
|
|
13959
14024
|
await access(path3, constants.W_OK);
|
|
13960
14025
|
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
13961
|
-
const
|
|
13962
|
-
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
13963
|
-
const next = { ...current, [key]: Reflect.get(result.data, key) };
|
|
14026
|
+
const next = change(current);
|
|
13964
14027
|
validateCredentials(next);
|
|
14028
|
+
if (next === current) return;
|
|
13965
14029
|
const file = await open2(temporary, "wx", 384);
|
|
13966
14030
|
created = true;
|
|
13967
14031
|
try {
|
|
@@ -14022,22 +14086,43 @@ function tenantInputFailure(error) {
|
|
|
14022
14086
|
}
|
|
14023
14087
|
fail(error);
|
|
14024
14088
|
}
|
|
14025
|
-
var
|
|
14089
|
+
var COLUMNS = [
|
|
14026
14090
|
{ key: "name", label: "Tenant" },
|
|
14027
14091
|
{ key: "active", label: "Selected" },
|
|
14028
14092
|
{ key: "tsgId", label: "TSG ID" },
|
|
14029
14093
|
{ key: "configPath", label: "Config file" }
|
|
14030
14094
|
];
|
|
14095
|
+
var KEY_VALUE_COLUMNS = [
|
|
14096
|
+
{ key: "key", label: "Key" },
|
|
14097
|
+
{ key: "value", label: "Value" }
|
|
14098
|
+
];
|
|
14099
|
+
function resolveEntry(name) {
|
|
14100
|
+
const store = readTenantStore();
|
|
14101
|
+
const selected = name ?? store.active;
|
|
14102
|
+
if (!selected)
|
|
14103
|
+
throw new Error(
|
|
14104
|
+
store.tenants.length ? `No tenant selected; pass a name or run 'airs tenant switch <name>' (registered: ${store.tenants.map((entry2) => entry2.name).join(", ")})` : "No tenant selected; run 'airs tenant create <name>' first"
|
|
14105
|
+
);
|
|
14106
|
+
const entry = store.tenants.find((value) => value.name === selected);
|
|
14107
|
+
if (!entry) throw new Error("Tenant not found");
|
|
14108
|
+
return entry;
|
|
14109
|
+
}
|
|
14110
|
+
function redact(key, value) {
|
|
14111
|
+
return isTenantSecret(key) && value ? "[REDACTED]" : value ?? "";
|
|
14112
|
+
}
|
|
14031
14113
|
function registerTenantCommand(program) {
|
|
14032
|
-
const tenant = program.command("tenant").description("Create, configure and select
|
|
14114
|
+
const tenant = program.command("tenant").description("Create, configure and select tenants (the only configuration source)").addHelpText(
|
|
14033
14115
|
"after",
|
|
14034
14116
|
examples(
|
|
14035
14117
|
"airs tenant create development",
|
|
14118
|
+
"airs tenant switch development",
|
|
14036
14119
|
"airs tenant set development defaultOutput yaml",
|
|
14120
|
+
"airs tenant set development airsApiKey",
|
|
14121
|
+
"airs tenant unset development defaultOutput",
|
|
14122
|
+
"airs tenant get development mgmtTsgId",
|
|
14037
14123
|
"airs tenant create production --config /secure/production.json",
|
|
14038
|
-
"airs tenant switch production",
|
|
14039
14124
|
"airs tenant read",
|
|
14040
|
-
"airs tenant
|
|
14125
|
+
"airs tenant path"
|
|
14041
14126
|
)
|
|
14042
14127
|
);
|
|
14043
14128
|
tenant.command("create <name>").description(
|
|
@@ -14063,7 +14148,7 @@ function registerTenantCommand(program) {
|
|
|
14063
14148
|
}
|
|
14064
14149
|
}
|
|
14065
14150
|
);
|
|
14066
|
-
tenant.command("set <name> <key> [value]").description("Update one
|
|
14151
|
+
tenant.command("set <name> <key> [value]").description("Update one tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
|
|
14067
14152
|
async (name, key, value, opts) => {
|
|
14068
14153
|
try {
|
|
14069
14154
|
validateTenantSettingKey(key);
|
|
@@ -14083,14 +14168,32 @@ function registerTenantCommand(program) {
|
|
|
14083
14168
|
}
|
|
14084
14169
|
}
|
|
14085
14170
|
);
|
|
14086
|
-
tenant.command("
|
|
14087
|
-
|
|
14088
|
-
|
|
14171
|
+
tenant.command("unset <name> <key>").description("Remove one tenant setting so the default applies; credentials cannot be cleared").action(async (name, key) => {
|
|
14172
|
+
try {
|
|
14173
|
+
const removed = await unsetTenantSetting(name, key);
|
|
14174
|
+
if (removed) ui.success(`Removed ${key} from tenant ${name}; selection unchanged.`);
|
|
14175
|
+
else ui.info(`${key} is not set for tenant ${name} \u2014 nothing to do`);
|
|
14176
|
+
} catch (error) {
|
|
14177
|
+
fail(error);
|
|
14178
|
+
}
|
|
14179
|
+
});
|
|
14180
|
+
const get = tenant.command("get <name> <key>").description("Print one effective tenant setting; credential values are redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, key, opts) => {
|
|
14181
|
+
try {
|
|
14182
|
+
validateTenantSettingKey(key);
|
|
14183
|
+
const format = await resolveOutput(get, opts, { ignoreConfig: true });
|
|
14184
|
+
const entry = resolveEntry(name);
|
|
14185
|
+
const config = readTenantConfig(entry.configPath, entry.tsgId);
|
|
14186
|
+
const value = redact(key, config[key]);
|
|
14187
|
+
if (format === "pretty") console.log(String(value));
|
|
14188
|
+
else console.log(formatOutput([{ key, value }], KEY_VALUE_COLUMNS, format));
|
|
14189
|
+
} catch (error) {
|
|
14190
|
+
fail(error);
|
|
14191
|
+
}
|
|
14192
|
+
});
|
|
14193
|
+
tenant.command("switch <name>").description("Persist the selected tenant for subsequent commands").action(async (name) => {
|
|
14089
14194
|
try {
|
|
14090
14195
|
const entry = await switchTenant(name);
|
|
14091
|
-
ui.success(
|
|
14092
|
-
entry ? `Selected ${entry.name} (TSG ${entry.tsgId})` : "Selected default; explicit config/environment overrides still apply"
|
|
14093
|
-
);
|
|
14196
|
+
ui.success(`Selected ${entry.name} (TSG ${entry.tsgId})`);
|
|
14094
14197
|
} catch (error) {
|
|
14095
14198
|
fail(error);
|
|
14096
14199
|
}
|
|
@@ -14099,65 +14202,63 @@ function registerTenantCommand(program) {
|
|
|
14099
14202
|
try {
|
|
14100
14203
|
const format = await resolveOutput(list, opts, { ignoreConfig: true });
|
|
14101
14204
|
const store = readTenantStore();
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
...
|
|
14111
|
-
|
|
14112
|
-
|
|
14113
|
-
|
|
14114
|
-
console.log(formatOutput(rows, COLUMNS2, format === "pretty" ? "table" : format));
|
|
14205
|
+
if (store.tenants.length === 0 && format === "pretty") {
|
|
14206
|
+
ui.emptyList("tenants");
|
|
14207
|
+
ui.status("Run 'airs tenant create <name>' to register one.");
|
|
14208
|
+
return;
|
|
14209
|
+
}
|
|
14210
|
+
if (store.active === null && store.tenants.length)
|
|
14211
|
+
ui.status("No tenant selected; run 'airs tenant switch <name>'.");
|
|
14212
|
+
const rows = store.tenants.map((entry) => ({
|
|
14213
|
+
...entry,
|
|
14214
|
+
active: entry.name === store.active
|
|
14215
|
+
}));
|
|
14216
|
+
console.log(formatOutput(rows, COLUMNS, format === "pretty" ? "table" : format));
|
|
14115
14217
|
} catch (error) {
|
|
14116
14218
|
fail(error);
|
|
14117
14219
|
}
|
|
14118
14220
|
});
|
|
14119
|
-
const read = tenant.command("read [name]").description(
|
|
14221
|
+
const read = tenant.command("read [name]").description(
|
|
14222
|
+
"Read a tenant config with all credential values redacted; defaults to the selected tenant"
|
|
14223
|
+
).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
|
|
14120
14224
|
try {
|
|
14121
14225
|
const format = await resolveOutput(read, opts, { ignoreConfig: true });
|
|
14122
|
-
const
|
|
14123
|
-
const
|
|
14124
|
-
const entry = store.tenants.find((value) => value.name === selected);
|
|
14125
|
-
if (selected !== "default" && !entry) throw new Error("Tenant not found");
|
|
14126
|
-
const path3 = entry?.configPath ?? expandConfigPath(process.env.PRISMA_AIRS_CONFIG_PATH || defaultTenantConfigPath());
|
|
14127
|
-
const config = readTenantConfig(path3, entry?.tsgId);
|
|
14226
|
+
const entry = resolveEntry(name);
|
|
14227
|
+
const config = readTenantConfig(entry.configPath, entry.tsgId);
|
|
14128
14228
|
const rows = Object.entries(config).map(([key, value]) => ({
|
|
14129
14229
|
key,
|
|
14130
|
-
value:
|
|
14230
|
+
value: redact(key, value)
|
|
14131
14231
|
}));
|
|
14132
|
-
ui.status(`Tenant ${
|
|
14133
|
-
console.log(
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
)
|
|
14142
|
-
);
|
|
14232
|
+
ui.status(`Tenant ${entry.name} (TSG ${entry.tsgId}): ${entry.configPath}`);
|
|
14233
|
+
console.log(formatOutput(rows, KEY_VALUE_COLUMNS, format === "pretty" ? "table" : format));
|
|
14234
|
+
} catch (error) {
|
|
14235
|
+
fail(error);
|
|
14236
|
+
}
|
|
14237
|
+
});
|
|
14238
|
+
tenant.command("path [name]").description("Print the config file path of a tenant; defaults to the selected tenant").action((name) => {
|
|
14239
|
+
try {
|
|
14240
|
+
console.log(resolveEntry(name).configPath);
|
|
14143
14241
|
} catch (error) {
|
|
14144
14242
|
fail(error);
|
|
14145
14243
|
}
|
|
14146
14244
|
});
|
|
14147
|
-
tenant.command("delete <name>").description(
|
|
14245
|
+
tenant.command("delete <name>").description(
|
|
14246
|
+
"Unregister a tenant; retain its source config file and clear the selection if it was selected"
|
|
14247
|
+
).option("--force", "Skip interactive confirmation").action(async (name, opts) => {
|
|
14148
14248
|
try {
|
|
14149
14249
|
const store = readTenantStore();
|
|
14150
|
-
if (name === "default" || name === store.active)
|
|
14151
|
-
throw new Error("Switch away first; default and active tenants cannot be deleted");
|
|
14152
14250
|
if (!store.tenants.some((entry) => entry.name === name))
|
|
14153
14251
|
throw new Error("Tenant not found");
|
|
14252
|
+
const selected = store.active === name;
|
|
14154
14253
|
await confirmOrAbort(
|
|
14155
|
-
`Unregister tenant ${name}? Its config file will be retained.`,
|
|
14254
|
+
`Unregister tenant ${name}?${selected ? " It is the selected tenant; no tenant will be selected afterwards." : ""} Its config file will be retained.`,
|
|
14156
14255
|
Boolean(opts.force),
|
|
14157
14256
|
{ action: `unregister tenant ${name}` }
|
|
14158
14257
|
);
|
|
14159
|
-
await deleteTenant(name);
|
|
14160
|
-
ui.success(
|
|
14258
|
+
const { selectionCleared } = await deleteTenant(name);
|
|
14259
|
+
ui.success(
|
|
14260
|
+
`Unregistered ${name}; source config file retained${selectionCleared ? "; no tenant is selected" : ""}`
|
|
14261
|
+
);
|
|
14161
14262
|
} catch (error) {
|
|
14162
14263
|
fail(error);
|
|
14163
14264
|
}
|
|
@@ -14218,7 +14319,7 @@ function applySortedHelp(cmd) {
|
|
|
14218
14319
|
for (const sub of cmd.commands) applySortedHelp(sub);
|
|
14219
14320
|
}
|
|
14220
14321
|
function buildProgram() {
|
|
14221
|
-
const here =
|
|
14322
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
14222
14323
|
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
14223
14324
|
const program = new Command();
|
|
14224
14325
|
let dlpDebugBodyEnvironment;
|
|
@@ -14299,7 +14400,6 @@ function buildProgram() {
|
|
|
14299
14400
|
registerModelSecurityCommand(program);
|
|
14300
14401
|
registerAgentGuardCommand(program);
|
|
14301
14402
|
registerAiGatewayCommand(program);
|
|
14302
|
-
registerConfigCommand(program);
|
|
14303
14403
|
registerTenantCommand(program);
|
|
14304
14404
|
registerDoctorCommand(program);
|
|
14305
14405
|
registerCompletionCommand(program);
|