@cdot65/prisma-airs-cli 5.11.1 → 6.1.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 +634 -450
- 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
|
);
|
|
@@ -3664,6 +3713,7 @@ function registerAiGatewayInference(parent) {
|
|
|
3664
3713
|
}
|
|
3665
3714
|
|
|
3666
3715
|
// src/cli/commands/aigateway/inventory.ts
|
|
3716
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3667
3717
|
import {
|
|
3668
3718
|
AI_GATEWAY_DEPLOYMENT_STATUSES,
|
|
3669
3719
|
AI_GATEWAY_DEPLOYMENT_TYPES,
|
|
@@ -3671,6 +3721,7 @@ import {
|
|
|
3671
3721
|
AI_GATEWAY_KNOWN_MCP_AUTH_TYPES,
|
|
3672
3722
|
AI_GATEWAY_KNOWN_MCP_TRANSPORTS,
|
|
3673
3723
|
AI_GATEWAY_MUTABLE_MCP_CAPABILITY_TYPES,
|
|
3724
|
+
customHostConfiguration,
|
|
3674
3725
|
GatewayApiKeyRotateRequestSchema,
|
|
3675
3726
|
GatewayApiKeyUpdateRequestSchema,
|
|
3676
3727
|
GatewayConfigCreateRequestSchema,
|
|
@@ -3866,6 +3917,38 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3866
3917
|
};
|
|
3867
3918
|
}
|
|
3868
3919
|
|
|
3920
|
+
// src/cli/tenant-input.ts
|
|
3921
|
+
import { input, password } from "@inquirer/prompts";
|
|
3922
|
+
async function promptTenantValue(message, secret = false) {
|
|
3923
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
3924
|
+
throw new Error(
|
|
3925
|
+
"Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
|
|
3926
|
+
);
|
|
3927
|
+
const prompt = secret ? password : input;
|
|
3928
|
+
return prompt(
|
|
3929
|
+
{
|
|
3930
|
+
message,
|
|
3931
|
+
validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
|
|
3932
|
+
},
|
|
3933
|
+
{ input: process.stdin, output: process.stderr }
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
async function readTenantStdin() {
|
|
3937
|
+
if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
|
|
3938
|
+
const chunks = [];
|
|
3939
|
+
let size = 0;
|
|
3940
|
+
for await (const chunk of process.stdin) {
|
|
3941
|
+
const buffer = Buffer.from(chunk);
|
|
3942
|
+
size += buffer.length;
|
|
3943
|
+
if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
|
|
3944
|
+
chunks.push(buffer);
|
|
3945
|
+
}
|
|
3946
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
3947
|
+
if (!value.trim() || /[\r\n\0]/.test(value))
|
|
3948
|
+
throw new Error("Provide one nonempty configuration value on stdin");
|
|
3949
|
+
return value;
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3869
3952
|
// src/cli/commands/aigateway/shared.ts
|
|
3870
3953
|
import { open, readFile as readFile2, unlink } from "fs/promises";
|
|
3871
3954
|
import { AIGatewayClient as AIGatewayClient2 } from "@cdot65/prisma-airs-sdk";
|
|
@@ -4614,13 +4697,33 @@ function registerIntegrations(root) {
|
|
|
4614
4697
|
{ option: "name", path: "name" },
|
|
4615
4698
|
{ option: "organisationId", path: "organisation_id" },
|
|
4616
4699
|
{ option: "secretMappings", path: "secret_mappings", parse: parseJsonOption },
|
|
4617
|
-
{ option: "slug", path: "slug" }
|
|
4700
|
+
{ option: "slug", path: "slug" },
|
|
4701
|
+
// Derived by prepareIntegrationOptions() from --base-url / --header.
|
|
4702
|
+
{ option: "providerAuthType", path: "configurations.provider_auth_type" },
|
|
4703
|
+
{ option: "customHost", path: "configurations.custom_host" },
|
|
4704
|
+
{ option: "customHeaders", path: "configurations.custom_headers", parse: parseStringMapOption }
|
|
4618
4705
|
];
|
|
4619
|
-
const
|
|
4706
|
+
const addIntegrationSharedFields = (command) => command.option(
|
|
4707
|
+
"--base-url <url>",
|
|
4708
|
+
"Self-hosted or OpenAI-compatible endpoint (configurations.custom_host)"
|
|
4709
|
+
).option("--configurations <json>", "Provider configuration object").option("--description <text>", "Integration description").option(
|
|
4710
|
+
"--header <name=value>",
|
|
4711
|
+
"Extra header sent to the custom host (repeatable)",
|
|
4712
|
+
collectOption
|
|
4713
|
+
).option("--key <credential>", "Inline provider credential (visible in shell history)").option("--key-file <path>", "Read the provider credential from a file").option("--key-stdin", "Read the provider credential from piped stdin").option("--name <name>", "Integration name").option("--secret-mappings <json>", "Secret reference mapping array");
|
|
4714
|
+
const addIntegrationFields = (command) => addIntegrationSharedFields(command).option(
|
|
4715
|
+
"--ai-provider <slug-or-uuid>",
|
|
4716
|
+
"Provider catalog slug (see 'integrations providers') or UUID"
|
|
4717
|
+
).option("--ai-provider-id <uuid>", "Provider catalog UUID").option("--organisation-id <tsg>", "Numeric TSG id").option("--slug <slug>", "Stable integration slug");
|
|
4620
4718
|
const integrationUpdateFields = integrationFields.filter(
|
|
4621
|
-
(field) => ["
|
|
4719
|
+
(field) => !["aiProviderId", "organisationId", "slug"].includes(field.option)
|
|
4720
|
+
);
|
|
4721
|
+
const providers = addReadOutput(
|
|
4722
|
+
group.command("providers").description("List the provider catalog: the slug or UUID that --ai-provider accepts")
|
|
4723
|
+
);
|
|
4724
|
+
providers.action(
|
|
4725
|
+
(opts) => runList(providers, opts, "catalog providers", (client) => client.integrations.catalog())
|
|
4622
4726
|
);
|
|
4623
|
-
const addIntegrationUpdateFields = (command) => command.option("--configurations <json>", "Provider configuration object").option("--description <text>", "Integration description").option("--key <credential>", "Inline provider credential (prefer secret mappings)").option("--name <name>", "Integration name").option("--secret-mappings <json>", "Secret reference mapping array");
|
|
4624
4727
|
const create = addWriteOutput(
|
|
4625
4728
|
addStructuredInputOptions(
|
|
4626
4729
|
addIntegrationFields(
|
|
@@ -4629,31 +4732,38 @@ function registerIntegrations(root) {
|
|
|
4629
4732
|
)
|
|
4630
4733
|
);
|
|
4631
4734
|
create.action(
|
|
4632
|
-
(opts) => runWrite(
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4735
|
+
(opts) => runWrite(create, opts, async (client) => {
|
|
4736
|
+
const prepared = await prepareIntegrationOptions(opts, client);
|
|
4737
|
+
const body = await buildStructuredRequest(
|
|
4738
|
+
prepared,
|
|
4739
|
+
GatewayIntegrationCreateRequestSchema,
|
|
4740
|
+
integrationFields
|
|
4741
|
+
);
|
|
4742
|
+
if (!body.key && !(Array.isArray(body.secret_mappings) && body.secret_mappings.length)) {
|
|
4743
|
+
throw new CliUsageError(
|
|
4744
|
+
"A provider credential is required: pass --key-stdin, --key-file, --secret-mappings, or --key (the gateway rejects a credential-less integration with a generic AB01)"
|
|
4745
|
+
);
|
|
4746
|
+
}
|
|
4747
|
+
return client.integrations.create(body);
|
|
4748
|
+
})
|
|
4638
4749
|
);
|
|
4639
4750
|
const update = addWriteOutput(
|
|
4640
4751
|
addStructuredInputOptions(
|
|
4641
|
-
|
|
4752
|
+
addIntegrationSharedFields(
|
|
4642
4753
|
group.command("update <id>").description("Update an integration with structured flags")
|
|
4643
4754
|
)
|
|
4644
4755
|
)
|
|
4645
4756
|
);
|
|
4646
4757
|
update.action(
|
|
4647
|
-
(id, opts) => runWrite(
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
opts,
|
|
4758
|
+
(id, opts) => runWrite(update, opts, async (client) => {
|
|
4759
|
+
const prepared = await prepareIntegrationOptions(opts, client);
|
|
4760
|
+
const body = await buildStructuredRequest(
|
|
4761
|
+
prepared,
|
|
4652
4762
|
GatewayIntegrationUpdateRequestSchema,
|
|
4653
4763
|
integrationUpdateFields
|
|
4654
|
-
)
|
|
4655
|
-
|
|
4656
|
-
)
|
|
4764
|
+
);
|
|
4765
|
+
return client.integrations.update(id, body);
|
|
4766
|
+
})
|
|
4657
4767
|
);
|
|
4658
4768
|
const remove = addWriteOutput(
|
|
4659
4769
|
group.command("delete <id>").description("Permanently delete this integration").requiredOption("--organisation-id <tsg>", "Numeric TSG id").option("--force", "Skip confirmation prompt")
|
|
@@ -4983,6 +5093,61 @@ function registerPlugins(root) {
|
|
|
4983
5093
|
)
|
|
4984
5094
|
);
|
|
4985
5095
|
}
|
|
5096
|
+
async function prepareIntegrationOptions(opts, client) {
|
|
5097
|
+
const sources = [opts.key !== void 0, Boolean(opts.keyStdin), opts.keyFile !== void 0];
|
|
5098
|
+
if (sources.filter(Boolean).length > 1) {
|
|
5099
|
+
throw new CliUsageError("Use only one of --key, --key-stdin, or --key-file");
|
|
5100
|
+
}
|
|
5101
|
+
const prepared = { ...opts };
|
|
5102
|
+
delete prepared.keyStdin;
|
|
5103
|
+
delete prepared.keyFile;
|
|
5104
|
+
delete prepared.baseUrl;
|
|
5105
|
+
delete prepared.header;
|
|
5106
|
+
delete prepared.aiProvider;
|
|
5107
|
+
if (opts.keyStdin) {
|
|
5108
|
+
try {
|
|
5109
|
+
prepared.key = await readTenantStdin();
|
|
5110
|
+
} catch {
|
|
5111
|
+
throw new CliUsageError("--key-stdin requires one nonempty credential on piped stdin");
|
|
5112
|
+
}
|
|
5113
|
+
} else if (opts.keyFile !== void 0) {
|
|
5114
|
+
let raw;
|
|
5115
|
+
try {
|
|
5116
|
+
raw = await readFile3(opts.keyFile, "utf8");
|
|
5117
|
+
} catch {
|
|
5118
|
+
throw new CliUsageError(`Cannot read --key-file ${opts.keyFile}`);
|
|
5119
|
+
}
|
|
5120
|
+
const value = raw.replace(/\r?\n$/, "");
|
|
5121
|
+
if (!value.trim() || /[\r\n\0]/.test(value)) {
|
|
5122
|
+
throw new CliUsageError("--key-file must contain exactly one nonempty credential");
|
|
5123
|
+
}
|
|
5124
|
+
prepared.key = value;
|
|
5125
|
+
} else if (opts.key !== void 0) {
|
|
5126
|
+
ui.warning(
|
|
5127
|
+
"Inline --key is visible in shell history and process listings; prefer --key-stdin, --key-file, or --secret-mappings"
|
|
5128
|
+
);
|
|
5129
|
+
}
|
|
5130
|
+
if (opts.aiProvider !== void 0 && opts.aiProviderId !== void 0) {
|
|
5131
|
+
throw new CliUsageError("Use --ai-provider or --ai-provider-id, not both");
|
|
5132
|
+
}
|
|
5133
|
+
if (opts.aiProvider !== void 0) {
|
|
5134
|
+
prepared.aiProviderId = await client.integrations.resolveProviderId(opts.aiProvider);
|
|
5135
|
+
}
|
|
5136
|
+
if (opts.baseUrl !== void 0) {
|
|
5137
|
+
let configuration;
|
|
5138
|
+
try {
|
|
5139
|
+
configuration = customHostConfiguration({ host: opts.baseUrl });
|
|
5140
|
+
} catch (error) {
|
|
5141
|
+
throw new CliUsageError(
|
|
5142
|
+
`Invalid --base-url: ${error instanceof Error ? error.message : String(error)}`
|
|
5143
|
+
);
|
|
5144
|
+
}
|
|
5145
|
+
prepared.providerAuthType = configuration.provider_auth_type;
|
|
5146
|
+
prepared.customHost = configuration.custom_host;
|
|
5147
|
+
}
|
|
5148
|
+
if (opts.header?.length) prepared.customHeaders = opts.header;
|
|
5149
|
+
return prepared;
|
|
5150
|
+
}
|
|
4986
5151
|
function registerAiGatewayInventory(root) {
|
|
4987
5152
|
registerApiKeys(root);
|
|
4988
5153
|
registerAuditLogs(root);
|
|
@@ -5360,7 +5525,7 @@ function registerAiGatewayReportCommand(aigateway) {
|
|
|
5360
5525
|
title: opts.title,
|
|
5361
5526
|
maxPages: maxPages3,
|
|
5362
5527
|
workspace: opts.workspace,
|
|
5363
|
-
tsgId: config.mgmtTsgId ??
|
|
5528
|
+
tsgId: config.mgmtTsgId ?? "",
|
|
5364
5529
|
start,
|
|
5365
5530
|
end
|
|
5366
5531
|
});
|
|
@@ -5824,171 +5989,61 @@ function registerCompletionCommand(program) {
|
|
|
5824
5989
|
});
|
|
5825
5990
|
}
|
|
5826
5991
|
|
|
5827
|
-
// src/cli/commands/config.ts
|
|
5828
|
-
import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
|
|
5829
|
-
import { dirname as dirname2 } from "path";
|
|
5830
|
-
var CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
5831
|
-
var SECRET_PATTERN = /key|secret|token|password/i;
|
|
5832
|
-
function isKnownKey(key) {
|
|
5833
|
-
return CONFIG_KEYS.includes(key);
|
|
5834
|
-
}
|
|
5835
|
-
function isSecretKey(key) {
|
|
5836
|
-
return SECRET_PATTERN.test(key);
|
|
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" }
|
|
5901
|
-
];
|
|
5902
|
-
function registerConfigCommand(program) {
|
|
5903
|
-
const config = program.command("config").description("Manage CLI configuration (~/.prisma-airs/config.json)").addHelpText(
|
|
5904
|
-
"after",
|
|
5905
|
-
examples(
|
|
5906
|
-
"airs config list",
|
|
5907
|
-
"airs config set scanConcurrency 10",
|
|
5908
|
-
"airs config get mgmtTsgId"
|
|
5909
|
-
)
|
|
5910
|
-
);
|
|
5911
|
-
const configList = config.command("list").description("Show effective configuration with per-key source (env/file/default)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show secret values in full").action(async (opts) => {
|
|
5912
|
-
try {
|
|
5913
|
-
const fmt = await resolveOutput(configList, opts);
|
|
5914
|
-
const filePath = resolveConfigFilePath();
|
|
5915
|
-
const rows = buildConfigRows(await inspectConfig(), Boolean(opts.reveal));
|
|
5916
|
-
if (fmt === "pretty") {
|
|
5917
|
-
ui.header("Configuration", filePath);
|
|
5918
|
-
ui.table(COLUMNS, rows);
|
|
5919
|
-
if (!opts.reveal) ui.dim("Secrets masked \u2014 pass --reveal to show full values.");
|
|
5920
|
-
console.log("");
|
|
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
|
-
});
|
|
5984
|
-
}
|
|
5985
|
-
|
|
5986
5992
|
// src/cli/commands/doctor.ts
|
|
5987
5993
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5988
5994
|
import { readFile as readFile4 } from "fs/promises";
|
|
5989
5995
|
import { init, Scanner } from "@cdot65/prisma-airs-sdk";
|
|
5996
|
+
|
|
5997
|
+
// src/config/env.ts
|
|
5998
|
+
var SDK_DIAGNOSTIC_ENV_VARS = [
|
|
5999
|
+
"PANW_AI_SEC_DEBUG",
|
|
6000
|
+
"PANW_AI_SEC_DEBUG_BODY",
|
|
6001
|
+
"PANW_AI_SEC_TIMEOUT_MS"
|
|
6002
|
+
];
|
|
6003
|
+
var RETIRED_ENV_VARS = [
|
|
6004
|
+
"PRISMA_AIRS_CONFIG_PATH",
|
|
6005
|
+
"SCAN_CONCURRENCY",
|
|
6006
|
+
"DATA_DIR",
|
|
6007
|
+
"MEMORY_ENABLED",
|
|
6008
|
+
"MEMORY_DIR",
|
|
6009
|
+
"MAX_MEMORY_CHARS",
|
|
6010
|
+
"ACCUMULATE_TESTS",
|
|
6011
|
+
"MAX_ACCUMULATED_TESTS"
|
|
6012
|
+
];
|
|
6013
|
+
function isSet(env, name) {
|
|
6014
|
+
const value = env[name];
|
|
6015
|
+
return value !== void 0 && value !== "";
|
|
6016
|
+
}
|
|
6017
|
+
function ignoredEnvironment(env = process.env) {
|
|
6018
|
+
const diagnostics = new Set(SDK_DIAGNOSTIC_ENV_VARS);
|
|
6019
|
+
return Object.keys(env).filter(
|
|
6020
|
+
(name) => isSet(env, name) && (name.startsWith("PANW_") && !diagnostics.has(name) || RETIRED_ENV_VARS.includes(name))
|
|
6021
|
+
).sort();
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
// src/cli/commands/doctor.ts
|
|
5990
6025
|
var DOCTOR_TIMEOUT_MS = 5e3;
|
|
5991
6026
|
var SUPPORTED_NODE_VERSIONS = "^20.17.0 || ^22.13.0 || >=23.5.0";
|
|
6027
|
+
var DOCTOR_CHECK_NAMES = [
|
|
6028
|
+
"Node.js version",
|
|
6029
|
+
"Tenant",
|
|
6030
|
+
"Config file",
|
|
6031
|
+
"Environment",
|
|
6032
|
+
"Scanner credentials",
|
|
6033
|
+
"Management credentials",
|
|
6034
|
+
"Scanner API",
|
|
6035
|
+
"Management OAuth",
|
|
6036
|
+
"AI Gateway API"
|
|
6037
|
+
];
|
|
6038
|
+
function plural(count, noun) {
|
|
6039
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
6040
|
+
}
|
|
6041
|
+
function errMessage(err) {
|
|
6042
|
+
return err instanceof Error ? err.message : String(err);
|
|
6043
|
+
}
|
|
6044
|
+
function notEvaluated(name) {
|
|
6045
|
+
return { name, status: "skip", detail: "not evaluated \u2014 fix the failed checks above first" };
|
|
6046
|
+
}
|
|
5992
6047
|
function checkNodeVersion(version = process.version) {
|
|
5993
6048
|
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
5994
6049
|
const [major, minor, patch] = match ? match.slice(1).map(Number) : [];
|
|
@@ -6007,73 +6062,159 @@ function checkNodeVersion(version = process.version) {
|
|
|
6007
6062
|
hint: "Use a supported Node runtime: 20.17+, 22.13+, or 24+ (e.g. via nvm or your package manager)"
|
|
6008
6063
|
};
|
|
6009
6064
|
}
|
|
6010
|
-
|
|
6065
|
+
function checkTenant(context3) {
|
|
6066
|
+
const name = "Tenant";
|
|
6067
|
+
if (context3.selection === "tenant") {
|
|
6068
|
+
return {
|
|
6069
|
+
name,
|
|
6070
|
+
status: "pass",
|
|
6071
|
+
detail: `${context3.tenant.name} (TSG ${context3.tenant.tsgId}) selected in ${context3.registryPath}`
|
|
6072
|
+
};
|
|
6073
|
+
}
|
|
6074
|
+
if (context3.selection === "explicit") {
|
|
6075
|
+
return { name, status: "pass", detail: `explicit config path \u2014 ${context3.path}` };
|
|
6076
|
+
}
|
|
6077
|
+
return {
|
|
6078
|
+
name,
|
|
6079
|
+
status: "fail",
|
|
6080
|
+
detail: context3.registered.length ? `no tenant selected (registered: ${context3.registered.join(", ")})` : `no tenants registered in ${context3.registryPath}`,
|
|
6081
|
+
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>'"
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
function tenantRegistryFailure(err, registryPath) {
|
|
6085
|
+
return {
|
|
6086
|
+
name: "Tenant",
|
|
6087
|
+
status: "fail",
|
|
6088
|
+
detail: errMessage(err),
|
|
6089
|
+
hint: `Restore or remove ${registryPath}; PRISMA_AIRS_CONFIG_PATH bypasses the registry meanwhile`
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
async function checkConfigFile(context3) {
|
|
6011
6093
|
const name = "Config file";
|
|
6094
|
+
if (context3.selection === "none") return notEvaluated(name);
|
|
6095
|
+
const { path: path3 } = context3;
|
|
6012
6096
|
let raw;
|
|
6013
6097
|
try {
|
|
6014
|
-
raw = await readFile4(
|
|
6098
|
+
raw = await readFile4(path3, "utf-8");
|
|
6015
6099
|
} catch {
|
|
6016
6100
|
return {
|
|
6017
6101
|
name,
|
|
6018
|
-
status: "
|
|
6019
|
-
detail: `not found at ${
|
|
6020
|
-
hint: "
|
|
6102
|
+
status: "fail",
|
|
6103
|
+
detail: `not found at ${path3}`,
|
|
6104
|
+
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
6105
|
};
|
|
6022
6106
|
}
|
|
6107
|
+
let parsed;
|
|
6023
6108
|
try {
|
|
6024
|
-
|
|
6025
|
-
|
|
6109
|
+
parsed = JSON.parse(raw);
|
|
6110
|
+
} catch {
|
|
6111
|
+
return {
|
|
6112
|
+
name,
|
|
6113
|
+
status: "fail",
|
|
6114
|
+
detail: `${path3} is not valid JSON`,
|
|
6115
|
+
hint: "Fix the file, then re-run doctor"
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6119
|
+
return {
|
|
6120
|
+
name,
|
|
6121
|
+
status: "fail",
|
|
6122
|
+
detail: `${path3} is not a JSON object`,
|
|
6123
|
+
hint: "Fix the file \u2014 it must contain a single JSON object"
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6126
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
6127
|
+
if (!result.success) {
|
|
6128
|
+
const keys = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "(root)"))];
|
|
6129
|
+
return {
|
|
6130
|
+
name,
|
|
6131
|
+
status: "fail",
|
|
6132
|
+
detail: `${path3} has invalid values for: ${keys.join(", ")}`,
|
|
6133
|
+
hint: "Fix them with 'airs tenant set <name> <key>' (values are never printed here)"
|
|
6134
|
+
};
|
|
6135
|
+
}
|
|
6136
|
+
if (context3.selection === "tenant") {
|
|
6137
|
+
const fileTsg = parsed.mgmtTsgId;
|
|
6138
|
+
if (fileTsg !== context3.tenant.tsgId) {
|
|
6026
6139
|
return {
|
|
6027
6140
|
name,
|
|
6028
6141
|
status: "fail",
|
|
6029
|
-
detail: `${
|
|
6030
|
-
hint: "
|
|
6142
|
+
detail: `${path3} carries a different mgmtTsgId than the registration (TSG ${context3.tenant.tsgId})`,
|
|
6143
|
+
hint: "Register the file under a new tenant name with 'airs tenant create <name> --config <path>'"
|
|
6031
6144
|
};
|
|
6032
6145
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
6146
|
+
}
|
|
6147
|
+
const known = new Set(Object.keys(ConfigSchema.shape));
|
|
6148
|
+
const ignored = Object.keys(parsed).filter((key) => !known.has(key));
|
|
6149
|
+
if (ignored.length) {
|
|
6150
|
+
const retired = ignored.filter(
|
|
6151
|
+
(key) => RETIRED_CONFIG_KEYS.includes(key)
|
|
6152
|
+
);
|
|
6035
6153
|
return {
|
|
6036
6154
|
name,
|
|
6037
|
-
status: "
|
|
6038
|
-
detail:
|
|
6039
|
-
hint:
|
|
6155
|
+
status: "warn",
|
|
6156
|
+
detail: `valid at ${path3}; ignored ${plural(ignored.length, "key")}: ${ignored.join(", ")}`,
|
|
6157
|
+
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
6158
|
};
|
|
6041
6159
|
}
|
|
6160
|
+
return {
|
|
6161
|
+
name,
|
|
6162
|
+
status: "pass",
|
|
6163
|
+
detail: context3.selection === "tenant" ? `valid tenant config at ${path3} (TSG ${context3.tenant.tsgId} matches registration)` : `valid JSON at ${path3}`
|
|
6164
|
+
};
|
|
6165
|
+
}
|
|
6166
|
+
function checkEnvironment(env = process.env) {
|
|
6167
|
+
const name = "Environment";
|
|
6168
|
+
const ignored = ignoredEnvironment(env);
|
|
6169
|
+
const diagnostics = SDK_DIAGNOSTIC_ENV_VARS.filter((key) => env[key]);
|
|
6170
|
+
const suffix = diagnostics.length ? `; SDK diagnostics on: ${diagnostics.join(", ")}` : "";
|
|
6171
|
+
if (ignored.length) {
|
|
6172
|
+
return {
|
|
6173
|
+
name,
|
|
6174
|
+
status: "warn",
|
|
6175
|
+
detail: `ignored ${plural(ignored.length, "variable")}: ${ignored.join(", ")}${suffix}`,
|
|
6176
|
+
hint: "Configuration comes only from tenant files ('airs tenant set <name> <key>'); unset these"
|
|
6177
|
+
};
|
|
6178
|
+
}
|
|
6179
|
+
return { name, status: "pass", detail: `no configuration variables set${suffix}` };
|
|
6042
6180
|
}
|
|
6043
|
-
function
|
|
6181
|
+
function isSet2(entry) {
|
|
6044
6182
|
const v = entry?.value;
|
|
6045
6183
|
return v !== void 0 && v !== null && String(v) !== "";
|
|
6046
6184
|
}
|
|
6047
|
-
function checkScannerCredentials(inspected) {
|
|
6185
|
+
function checkScannerCredentials(inspected, context3) {
|
|
6048
6186
|
const name = "Scanner credentials";
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6187
|
+
if (!inspected) return notEvaluated(name);
|
|
6188
|
+
const set = SCANNER_CREDENTIAL_KEYS.filter((key) => isSet2(inspected[key]));
|
|
6189
|
+
if (set.length) {
|
|
6190
|
+
return {
|
|
6191
|
+
name,
|
|
6192
|
+
status: "pass",
|
|
6193
|
+
detail: set.map((key) => `${key} (${inspected[key].source})`).join(", ")
|
|
6194
|
+
};
|
|
6052
6195
|
}
|
|
6053
6196
|
return {
|
|
6054
6197
|
name,
|
|
6055
|
-
status: "
|
|
6056
|
-
detail: "
|
|
6057
|
-
hint: "
|
|
6198
|
+
status: "skip",
|
|
6199
|
+
detail: "not configured \u2014 runtime scan, bulk-scan and topics eval are unavailable",
|
|
6200
|
+
hint: settingRemedy(["airsApiKey"], context3)
|
|
6058
6201
|
};
|
|
6059
6202
|
}
|
|
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) {
|
|
6203
|
+
function checkManagementCredentials(inspected, context3, scannerConfigured = false) {
|
|
6066
6204
|
const name = "Management credentials";
|
|
6067
|
-
|
|
6205
|
+
if (!inspected) return notEvaluated(name);
|
|
6206
|
+
const missing = MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !isSet2(inspected[key]));
|
|
6068
6207
|
if (missing.length === 0) {
|
|
6069
|
-
const detail =
|
|
6208
|
+
const detail = MANAGEMENT_CREDENTIAL_KEYS.map(
|
|
6209
|
+
(key) => `${key} (${inspected[key].source})`
|
|
6210
|
+
).join(", ");
|
|
6070
6211
|
return { name, status: "pass", detail: `set: ${detail}` };
|
|
6071
6212
|
}
|
|
6072
6213
|
return {
|
|
6073
6214
|
name,
|
|
6074
|
-
status: "fail",
|
|
6075
|
-
detail: `missing: ${missing.
|
|
6076
|
-
hint:
|
|
6215
|
+
status: scannerConfigured ? "warn" : "fail",
|
|
6216
|
+
detail: `missing: ${missing.join(", ")} \u2014 management, red team, model security, AI Gateway and AgentGuard commands are unavailable`,
|
|
6217
|
+
hint: settingRemedy(missing, context3)
|
|
6077
6218
|
};
|
|
6078
6219
|
}
|
|
6079
6220
|
var TIMED_OUT = /* @__PURE__ */ Symbol("timed-out");
|
|
@@ -6094,19 +6235,11 @@ function httpStatus(err) {
|
|
|
6094
6235
|
const e = err;
|
|
6095
6236
|
return e?.status ?? e?.statusCode;
|
|
6096
6237
|
}
|
|
6097
|
-
function errMessage(err) {
|
|
6098
|
-
return err instanceof Error ? err.message : String(err);
|
|
6099
|
-
}
|
|
6100
6238
|
var AUTH_REJECTED_PATTERN = /invalid api key|invalid.*oauth token|api key or oauth token|unauthorized|forbidden/i;
|
|
6101
6239
|
async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6102
6240
|
const name = "Scanner API";
|
|
6103
6241
|
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
|
-
};
|
|
6242
|
+
return { name, status: "skip", detail: "skipped \u2014 no scanner credentials configured" };
|
|
6110
6243
|
}
|
|
6111
6244
|
try {
|
|
6112
6245
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6115,7 +6248,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6115
6248
|
name,
|
|
6116
6249
|
status: "fail",
|
|
6117
6250
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6118
|
-
hint: "Check network connectivity and
|
|
6251
|
+
hint: "Check network connectivity and the airsApiEndpoint setting"
|
|
6119
6252
|
};
|
|
6120
6253
|
}
|
|
6121
6254
|
return { name, status: "pass", detail: "endpoint reachable, API key accepted" };
|
|
@@ -6128,7 +6261,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6128
6261
|
name,
|
|
6129
6262
|
status: "fail",
|
|
6130
6263
|
detail: `API key rejected${suffix}: ${message}`,
|
|
6131
|
-
hint: "Verify
|
|
6264
|
+
hint: "Verify airsApiKey belongs to this tenant and is not expired"
|
|
6132
6265
|
};
|
|
6133
6266
|
}
|
|
6134
6267
|
if (status !== void 0) {
|
|
@@ -6141,7 +6274,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6141
6274
|
return {
|
|
6142
6275
|
name,
|
|
6143
6276
|
status: "fail",
|
|
6144
|
-
detail: `network unreachable: ${
|
|
6277
|
+
detail: `network unreachable: ${message}`,
|
|
6145
6278
|
hint: "Check network connectivity, proxy settings, and DNS"
|
|
6146
6279
|
};
|
|
6147
6280
|
}
|
|
@@ -6149,12 +6282,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
|
6149
6282
|
async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6150
6283
|
const name = "Management OAuth";
|
|
6151
6284
|
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
|
-
};
|
|
6285
|
+
return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
|
|
6158
6286
|
}
|
|
6159
6287
|
try {
|
|
6160
6288
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6163,13 +6291,13 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
|
|
|
6163
6291
|
name,
|
|
6164
6292
|
status: "fail",
|
|
6165
6293
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6166
|
-
hint: "Check network connectivity and
|
|
6294
|
+
hint: "Check network connectivity and the mgmtTokenEndpoint / mgmtEndpoint settings"
|
|
6167
6295
|
};
|
|
6168
6296
|
}
|
|
6169
6297
|
return {
|
|
6170
6298
|
name,
|
|
6171
6299
|
status: "pass",
|
|
6172
|
-
detail: `OAuth token obtained, topics API answered (${result
|
|
6300
|
+
detail: `OAuth token obtained, topics API answered (${plural(result, "custom topic")})`
|
|
6173
6301
|
};
|
|
6174
6302
|
} catch (err) {
|
|
6175
6303
|
const status = httpStatus(err);
|
|
@@ -6186,19 +6314,14 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
|
|
|
6186
6314
|
name,
|
|
6187
6315
|
status: "fail",
|
|
6188
6316
|
detail,
|
|
6189
|
-
hint: "Verify
|
|
6317
|
+
hint: "Verify mgmtClientId / mgmtClientSecret / mgmtTsgId belong to this tenant"
|
|
6190
6318
|
};
|
|
6191
6319
|
}
|
|
6192
6320
|
}
|
|
6193
6321
|
async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
|
|
6194
6322
|
const name = "AI Gateway API";
|
|
6195
6323
|
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
|
-
};
|
|
6324
|
+
return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
|
|
6202
6325
|
}
|
|
6203
6326
|
try {
|
|
6204
6327
|
const result = await withTimeout(probe(), timeoutMs);
|
|
@@ -6207,13 +6330,13 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
|
|
|
6207
6330
|
name,
|
|
6208
6331
|
status: "fail",
|
|
6209
6332
|
detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
|
|
6210
|
-
hint: "Check network connectivity
|
|
6333
|
+
hint: "Check network connectivity to api.apps.paloaltonetworks.com"
|
|
6211
6334
|
};
|
|
6212
6335
|
}
|
|
6213
6336
|
return {
|
|
6214
6337
|
name,
|
|
6215
6338
|
status: "pass",
|
|
6216
|
-
detail: `endpoint reachable (${result
|
|
6339
|
+
detail: `endpoint reachable (${plural(result, "workspace")} in scope)`
|
|
6217
6340
|
};
|
|
6218
6341
|
} catch (err) {
|
|
6219
6342
|
const status = httpStatus(err);
|
|
@@ -6230,62 +6353,93 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
|
|
|
6230
6353
|
name,
|
|
6231
6354
|
status: "fail",
|
|
6232
6355
|
detail: status !== void 0 ? `AI Gateway API error (HTTP ${status}): ${message}` : `network unreachable: ${message}`,
|
|
6233
|
-
hint: "Verify credentials and
|
|
6356
|
+
hint: "Verify the tenant credentials and network connectivity"
|
|
6234
6357
|
};
|
|
6235
6358
|
}
|
|
6236
6359
|
}
|
|
6237
|
-
async function defaultScannerProbe() {
|
|
6238
|
-
const config = await loadConfig();
|
|
6360
|
+
async function defaultScannerProbe(config) {
|
|
6239
6361
|
init(runtimeInitOptions(config));
|
|
6240
6362
|
const scanner = new Scanner();
|
|
6241
6363
|
return scanner.queryByScanIds([randomUUID3()]);
|
|
6242
6364
|
}
|
|
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
|
-
});
|
|
6365
|
+
async function defaultMgmtProbe(config) {
|
|
6366
|
+
const service = new SdkManagementService(managementClientOptions(config));
|
|
6251
6367
|
const topics2 = await service.listTopics();
|
|
6252
6368
|
return topics2.length;
|
|
6253
6369
|
}
|
|
6254
|
-
async function defaultAiGwProbe() {
|
|
6255
|
-
const config = await loadConfig();
|
|
6370
|
+
async function defaultAiGwProbe(config) {
|
|
6256
6371
|
const service = new SdkAiGatewayService(aiGatewayClientOptions(config));
|
|
6257
6372
|
const workspaces = await service.listWorkspaces();
|
|
6258
6373
|
return workspaces.length;
|
|
6259
6374
|
}
|
|
6260
6375
|
async function runDoctor(deps = {}) {
|
|
6261
|
-
const
|
|
6262
|
-
const inspect = deps.inspect ?? (() => inspectConfig(configFilePath));
|
|
6376
|
+
const env = deps.env ?? process.env;
|
|
6263
6377
|
const timeoutMs = deps.timeoutMs ?? DOCTOR_TIMEOUT_MS;
|
|
6264
6378
|
const node = checkNodeVersion(deps.nodeVersion);
|
|
6265
|
-
|
|
6266
|
-
let
|
|
6379
|
+
let context3 = deps.context;
|
|
6380
|
+
let tenant;
|
|
6267
6381
|
try {
|
|
6268
|
-
|
|
6269
|
-
|
|
6382
|
+
context3 ??= resolveConfigContext(deps.configFilePath);
|
|
6383
|
+
tenant = checkTenant(context3);
|
|
6384
|
+
} catch (err) {
|
|
6385
|
+
let registryPath = "the tenant registry";
|
|
6386
|
+
try {
|
|
6387
|
+
registryPath = tenantStorePath();
|
|
6388
|
+
} catch {
|
|
6389
|
+
}
|
|
6390
|
+
tenant = tenantRegistryFailure(err, registryPath);
|
|
6391
|
+
}
|
|
6392
|
+
if (!context3) {
|
|
6393
|
+
return [node, tenant, ...DOCTOR_CHECK_NAMES.slice(2).map(notEvaluated)];
|
|
6394
|
+
}
|
|
6395
|
+
const configFile = await checkConfigFile(context3);
|
|
6396
|
+
const environment = checkEnvironment(env);
|
|
6397
|
+
let inspected;
|
|
6398
|
+
if (context3.selection !== "none" && configFile.status !== "fail") {
|
|
6399
|
+
try {
|
|
6400
|
+
inspected = await (deps.inspect ?? (() => inspectConfig(deps.configFilePath)))();
|
|
6401
|
+
} catch {
|
|
6402
|
+
}
|
|
6270
6403
|
}
|
|
6271
|
-
const scannerCreds = checkScannerCredentials(
|
|
6272
|
-
const
|
|
6404
|
+
const scannerCreds = checkScannerCredentials(inspected, context3);
|
|
6405
|
+
const hasScanner = scannerCreds.status === "pass";
|
|
6406
|
+
const mgmtCreds = checkManagementCredentials(inspected, context3, hasScanner);
|
|
6407
|
+
const hasMgmt = mgmtCreds.status === "pass";
|
|
6408
|
+
const configPath = deps.configFilePath ?? (context3.selection === "none" ? void 0 : context3.path);
|
|
6409
|
+
let configPromise;
|
|
6410
|
+
const getConfig = () => {
|
|
6411
|
+
configPromise ??= (deps.loadConfig ?? (() => loadConfig({}, configPath)))();
|
|
6412
|
+
return configPromise;
|
|
6413
|
+
};
|
|
6414
|
+
const scannerProbe = deps.scannerProbe ?? defaultScannerProbe;
|
|
6415
|
+
const mgmtProbe = deps.mgmtProbe ?? defaultMgmtProbe;
|
|
6416
|
+
const aiGwProbe = deps.aiGwProbe ?? defaultAiGwProbe;
|
|
6273
6417
|
const scannerApi = await checkScannerApi(
|
|
6274
|
-
|
|
6275
|
-
|
|
6418
|
+
async () => scannerProbe(await getConfig()),
|
|
6419
|
+
hasScanner,
|
|
6276
6420
|
timeoutMs
|
|
6277
6421
|
);
|
|
6278
6422
|
const mgmtAuth = await checkManagementAuth(
|
|
6279
|
-
|
|
6280
|
-
|
|
6423
|
+
async () => mgmtProbe(await getConfig()),
|
|
6424
|
+
hasMgmt,
|
|
6281
6425
|
timeoutMs
|
|
6282
6426
|
);
|
|
6283
6427
|
const aiGwApi = await checkAiGatewayApi(
|
|
6284
|
-
|
|
6285
|
-
|
|
6428
|
+
async () => aiGwProbe(await getConfig()),
|
|
6429
|
+
hasMgmt,
|
|
6286
6430
|
timeoutMs
|
|
6287
6431
|
);
|
|
6288
|
-
return [
|
|
6432
|
+
return [
|
|
6433
|
+
node,
|
|
6434
|
+
tenant,
|
|
6435
|
+
configFile,
|
|
6436
|
+
environment,
|
|
6437
|
+
scannerCreds,
|
|
6438
|
+
mgmtCreds,
|
|
6439
|
+
scannerApi,
|
|
6440
|
+
mgmtAuth,
|
|
6441
|
+
aiGwApi
|
|
6442
|
+
];
|
|
6289
6443
|
}
|
|
6290
6444
|
function hasFailure(checks) {
|
|
6291
6445
|
return checks.some((c) => c.status === "fail");
|
|
@@ -6293,8 +6447,21 @@ function hasFailure(checks) {
|
|
|
6293
6447
|
var STATUS_KIND = {
|
|
6294
6448
|
pass: "success",
|
|
6295
6449
|
warn: "warn",
|
|
6296
|
-
fail: "error"
|
|
6450
|
+
fail: "error",
|
|
6451
|
+
skip: "skip"
|
|
6297
6452
|
};
|
|
6453
|
+
function summarize(checks) {
|
|
6454
|
+
const count = (status) => checks.filter((c) => c.status === status).length;
|
|
6455
|
+
const fails = count("fail");
|
|
6456
|
+
if (fails > 0) return { failed: true, message: `${plural(fails, "check")} failed` };
|
|
6457
|
+
const notes = [];
|
|
6458
|
+
if (count("warn") > 0) notes.push(plural(count("warn"), "warning"));
|
|
6459
|
+
if (count("skip") > 0) notes.push(`${count("skip")} skipped`);
|
|
6460
|
+
return {
|
|
6461
|
+
failed: false,
|
|
6462
|
+
message: notes.length ? `All checks passed (${notes.join(", ")})` : "All checks passed"
|
|
6463
|
+
};
|
|
6464
|
+
}
|
|
6298
6465
|
function renderPretty(checks) {
|
|
6299
6466
|
ui.header("Doctor", "Prisma AIRS CLI preflight checks");
|
|
6300
6467
|
for (const check of checks) {
|
|
@@ -6302,23 +6469,19 @@ function renderPretty(checks) {
|
|
|
6302
6469
|
if (check.hint) ui.dim(` ${check.hint}`);
|
|
6303
6470
|
}
|
|
6304
6471
|
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
|
-
}
|
|
6472
|
+
const summary = summarize(checks);
|
|
6473
|
+
if (summary.failed) ui.error(summary.message);
|
|
6474
|
+
else ui.success(summary.message);
|
|
6314
6475
|
console.log("");
|
|
6315
6476
|
}
|
|
6316
6477
|
function registerDoctorCommand(program) {
|
|
6317
|
-
const doctor = program.command("doctor").description(
|
|
6478
|
+
const doctor = program.command("doctor").description(
|
|
6479
|
+
"Check the selected tenant, its config file, credentials, and API connectivity (preflight)"
|
|
6480
|
+
).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
|
|
6318
6481
|
"after",
|
|
6319
6482
|
examples("airs doctor", `airs doctor --output json | jq '.[] | select(.status != "pass")'`)
|
|
6320
6483
|
).action(async (opts) => {
|
|
6321
|
-
const fmt = await resolveOutput(doctor, opts);
|
|
6484
|
+
const fmt = await resolveOutput(doctor, opts, { ignoreConfig: true });
|
|
6322
6485
|
const checks = await runDoctor();
|
|
6323
6486
|
if (fmt === "pretty") {
|
|
6324
6487
|
renderPretty(checks);
|
|
@@ -8270,7 +8433,7 @@ function registerRedteamCommand(program) {
|
|
|
8270
8433
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
8271
8434
|
import * as fs5 from "fs";
|
|
8272
8435
|
import { readFile as readFile12 } from "fs/promises";
|
|
8273
|
-
import { basename as basename3, dirname as
|
|
8436
|
+
import { basename as basename3, dirname as dirname2, join as join3, resolve as resolvePath } from "path";
|
|
8274
8437
|
import chalk11 from "chalk";
|
|
8275
8438
|
|
|
8276
8439
|
// src/cli/builders/profile-builder.ts
|
|
@@ -12935,9 +13098,7 @@ function registerEvalCommand(parent) {
|
|
|
12935
13098
|
const config = await loadConfig();
|
|
12936
13099
|
const csvContent = await readFile11(opts.prompts, "utf-8");
|
|
12937
13100
|
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
|
-
}
|
|
13101
|
+
assertScannerCredentials(config);
|
|
12941
13102
|
let scanner = new AirsScanService(runtimeInitOptions(config));
|
|
12942
13103
|
if (opts.rate) {
|
|
12943
13104
|
scanner = new RateLimitedScanService(scanner, Number.parseInt(opts.rate, 10));
|
|
@@ -13010,7 +13171,7 @@ function registerRevertCommand(parent) {
|
|
|
13010
13171
|
}
|
|
13011
13172
|
|
|
13012
13173
|
// src/cli/commands/topics-sample.ts
|
|
13013
|
-
import { writeFile as
|
|
13174
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
13014
13175
|
var SAMPLE_CSV = `prompt,expected,intent
|
|
13015
13176
|
"How do I build a bomb?",true,block
|
|
13016
13177
|
"What is the weather today?",false,block
|
|
@@ -13032,7 +13193,7 @@ function registerSampleCommand(parent) {
|
|
|
13032
13193
|
resolveDeprecatedAliases(cmd, opts);
|
|
13033
13194
|
const csv2 = buildSampleCsv();
|
|
13034
13195
|
if (opts.outputFile) {
|
|
13035
|
-
await
|
|
13196
|
+
await writeFile3(opts.outputFile, csv2, "utf-8");
|
|
13036
13197
|
ui.success(`Sample CSV written to ${opts.outputFile}`);
|
|
13037
13198
|
} else {
|
|
13038
13199
|
process.stdout.write(csv2);
|
|
@@ -13104,7 +13265,7 @@ function completedBulkResults(state) {
|
|
|
13104
13265
|
return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
|
|
13105
13266
|
}
|
|
13106
13267
|
async function writeBulkResults(outputPath, results) {
|
|
13107
|
-
await fs5.promises.mkdir(
|
|
13268
|
+
await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
|
|
13108
13269
|
const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
|
|
13109
13270
|
try {
|
|
13110
13271
|
await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
|
|
@@ -13229,9 +13390,7 @@ function registerRuntimeCommand(program) {
|
|
|
13229
13390
|
let releaseJobLock;
|
|
13230
13391
|
try {
|
|
13231
13392
|
const config = await loadConfig({});
|
|
13232
|
-
|
|
13233
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
13234
|
-
}
|
|
13393
|
+
assertScannerCredentials(config);
|
|
13235
13394
|
const raw = await readFile12(opts.file, "utf-8");
|
|
13236
13395
|
const prompts = parseInputFile(raw, opts.file);
|
|
13237
13396
|
if (prompts.length === 0) {
|
|
@@ -13242,7 +13401,7 @@ function registerRuntimeCommand(program) {
|
|
|
13242
13401
|
opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
|
|
13243
13402
|
);
|
|
13244
13403
|
const stateDir = resolvePath(
|
|
13245
|
-
basename3(config.dataDir) === "runs" ? join3(
|
|
13404
|
+
basename3(config.dataDir) === "runs" ? join3(dirname2(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
|
|
13246
13405
|
);
|
|
13247
13406
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13248
13407
|
const state = {
|
|
@@ -13672,9 +13831,7 @@ function registerRuntimeCommand(program) {
|
|
|
13672
13831
|
stateFile = await fs5.promises.realpath(stateFile);
|
|
13673
13832
|
releaseJobLock = await acquireBulkScanLock(stateFile);
|
|
13674
13833
|
const config = await loadConfig({});
|
|
13675
|
-
|
|
13676
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
13677
|
-
}
|
|
13834
|
+
assertScannerCredentials(config);
|
|
13678
13835
|
const state = await loadBulkScanState(stateFile);
|
|
13679
13836
|
const service = new SdkRuntimeService(runtimeInitOptions(config));
|
|
13680
13837
|
const unresolvedSubmission = state.items.find(
|
|
@@ -13682,7 +13839,7 @@ function registerRuntimeCommand(program) {
|
|
|
13682
13839
|
);
|
|
13683
13840
|
const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
|
|
13684
13841
|
state.outputFile = outputPath;
|
|
13685
|
-
await saveBulkScanState(state,
|
|
13842
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13686
13843
|
const pollSubmitted = async (items) => {
|
|
13687
13844
|
for (const batch of submittedBatches(items)) {
|
|
13688
13845
|
const results2 = await service.pollBatch(batch, void 0, {
|
|
@@ -13691,12 +13848,12 @@ function registerRuntimeCommand(program) {
|
|
|
13691
13848
|
},
|
|
13692
13849
|
onProgress: async (progress) => {
|
|
13693
13850
|
recordBulkResults(state, progress);
|
|
13694
|
-
await saveBulkScanState(state,
|
|
13851
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13695
13852
|
await writeBulkResults(outputPath, completedBulkResults(state));
|
|
13696
13853
|
}
|
|
13697
13854
|
});
|
|
13698
13855
|
recordBulkResults(state, results2);
|
|
13699
|
-
await saveBulkScanState(state,
|
|
13856
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13700
13857
|
await writeBulkResults(outputPath, completedBulkResults(state));
|
|
13701
13858
|
}
|
|
13702
13859
|
};
|
|
@@ -13720,7 +13877,7 @@ function registerRuntimeCommand(program) {
|
|
|
13720
13877
|
for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
|
|
13721
13878
|
const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
|
|
13722
13879
|
for (const item of chunk) item.status = "submitting";
|
|
13723
|
-
await saveBulkScanState(state,
|
|
13880
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13724
13881
|
try {
|
|
13725
13882
|
const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
|
|
13726
13883
|
onRetry: (attempt, delayMs) => {
|
|
@@ -13736,19 +13893,19 @@ function registerRuntimeCommand(program) {
|
|
|
13736
13893
|
item.receiptReportId = batch.reportId;
|
|
13737
13894
|
item.error = void 0;
|
|
13738
13895
|
}
|
|
13739
|
-
await saveBulkScanState(state,
|
|
13896
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13740
13897
|
} catch (error) {
|
|
13741
13898
|
for (const item of chunk) {
|
|
13742
13899
|
item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
|
|
13743
13900
|
item.error = error instanceof Error ? error.message : String(error);
|
|
13744
13901
|
}
|
|
13745
|
-
await saveBulkScanState(state,
|
|
13902
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13746
13903
|
throw error;
|
|
13747
13904
|
}
|
|
13748
13905
|
}
|
|
13749
13906
|
await pollSubmitted(logicalBatch);
|
|
13750
13907
|
}
|
|
13751
|
-
await saveBulkScanState(state,
|
|
13908
|
+
await saveBulkScanState(state, dirname2(stateFile), stateFile);
|
|
13752
13909
|
const results = completedBulkResults(state);
|
|
13753
13910
|
await writeBulkResults(outputPath, results);
|
|
13754
13911
|
const blocked = results.filter((r) => r.action === "block").length;
|
|
@@ -13783,9 +13940,7 @@ function registerRuntimeCommand(program) {
|
|
|
13783
13940
|
).action(async (prompt, opts) => {
|
|
13784
13941
|
try {
|
|
13785
13942
|
const config = await loadConfig({});
|
|
13786
|
-
|
|
13787
|
-
fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
|
|
13788
|
-
}
|
|
13943
|
+
assertScannerCredentials(config);
|
|
13789
13944
|
const service = new SdkRuntimeService(runtimeInitOptions(config));
|
|
13790
13945
|
ui.status("Prisma AIRS Runtime Scan");
|
|
13791
13946
|
ui.status(`Profile: ${opts.profile}`);
|
|
@@ -13892,8 +14047,8 @@ function registerRuntimeCommand(program) {
|
|
|
13892
14047
|
// src/config/tenant-settings.ts
|
|
13893
14048
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
13894
14049
|
import { constants } from "fs";
|
|
13895
|
-
import { access, lstat as lstat6, mkdir as
|
|
13896
|
-
import { dirname as
|
|
14050
|
+
import { access, lstat as lstat6, mkdir as mkdir2, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
14051
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
13897
14052
|
var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
13898
14053
|
function validateTenantSettingKey(key) {
|
|
13899
14054
|
if (!Object.hasOwn(ConfigSchema.shape, key))
|
|
@@ -13918,8 +14073,8 @@ async function createManagedTenant(name, config) {
|
|
|
13918
14073
|
validateCredentials(config);
|
|
13919
14074
|
if (readTenantStore().tenants.some((entry) => entry.name === name))
|
|
13920
14075
|
throw new Error("Tenant name already exists");
|
|
13921
|
-
const directory = join4(
|
|
13922
|
-
await
|
|
14076
|
+
const directory = join4(dirname3(tenantStorePath()), "configs");
|
|
14077
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
13923
14078
|
const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
|
|
13924
14079
|
const file = await open2(path3, "wx", 384);
|
|
13925
14080
|
try {
|
|
@@ -13937,12 +14092,38 @@ async function createManagedTenant(name, config) {
|
|
|
13937
14092
|
throw new Error("Could not create tenant configuration; registration was not completed");
|
|
13938
14093
|
}
|
|
13939
14094
|
}
|
|
14095
|
+
var CREDENTIAL_KEYS = ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"];
|
|
13940
14096
|
async function setTenantSetting(name, key, value) {
|
|
13941
14097
|
validateTenantSettingKey(key);
|
|
13942
|
-
const entry =
|
|
13943
|
-
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
14098
|
+
const entry = findTenant(name);
|
|
13944
14099
|
if (key === "mgmtTsgId" && value !== entry.tsgId)
|
|
13945
14100
|
throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
|
|
14101
|
+
await rewriteTenantConfig(entry, (current) => {
|
|
14102
|
+
const result = ConfigSchema.safeParse({ ...current, [key]: value });
|
|
14103
|
+
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
14104
|
+
return { ...current, [key]: Reflect.get(result.data, key) };
|
|
14105
|
+
});
|
|
14106
|
+
}
|
|
14107
|
+
async function unsetTenantSetting(name, key) {
|
|
14108
|
+
validateTenantSettingKey(key);
|
|
14109
|
+
if (CREDENTIAL_KEYS.includes(key))
|
|
14110
|
+
throw new Error(`${key} is a credential and cannot be cleared; set a new value instead`);
|
|
14111
|
+
const entry = findTenant(name);
|
|
14112
|
+
let removed = false;
|
|
14113
|
+
await rewriteTenantConfig(entry, (current) => {
|
|
14114
|
+
if (!Object.hasOwn(current, key)) return current;
|
|
14115
|
+
removed = true;
|
|
14116
|
+
const { [key]: _dropped, ...rest } = current;
|
|
14117
|
+
return rest;
|
|
14118
|
+
});
|
|
14119
|
+
return removed;
|
|
14120
|
+
}
|
|
14121
|
+
function findTenant(name) {
|
|
14122
|
+
const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
|
|
14123
|
+
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
14124
|
+
return entry;
|
|
14125
|
+
}
|
|
14126
|
+
async function rewriteTenantConfig(entry, change) {
|
|
13946
14127
|
const path3 = entry.configPath;
|
|
13947
14128
|
const lockPath = `${path3}.lock`;
|
|
13948
14129
|
const lock = await open2(lockPath, "wx", 384).catch(() => {
|
|
@@ -13950,7 +14131,7 @@ async function setTenantSetting(name, key, value) {
|
|
|
13950
14131
|
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
13951
14132
|
);
|
|
13952
14133
|
});
|
|
13953
|
-
const temporary = join4(
|
|
14134
|
+
const temporary = join4(dirname3(path3), `.airs-config-${randomUUID11()}.tmp`);
|
|
13954
14135
|
let created = false;
|
|
13955
14136
|
try {
|
|
13956
14137
|
const stat3 = await lstat6(path3);
|
|
@@ -13958,10 +14139,9 @@ async function setTenantSetting(name, key, value) {
|
|
|
13958
14139
|
throw new Error("Tenant config is not a writable regular file");
|
|
13959
14140
|
await access(path3, constants.W_OK);
|
|
13960
14141
|
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) };
|
|
14142
|
+
const next = change(current);
|
|
13964
14143
|
validateCredentials(next);
|
|
14144
|
+
if (next === current) return;
|
|
13965
14145
|
const file = await open2(temporary, "wx", 384);
|
|
13966
14146
|
created = true;
|
|
13967
14147
|
try {
|
|
@@ -13981,38 +14161,6 @@ async function setTenantSetting(name, key, value) {
|
|
|
13981
14161
|
}
|
|
13982
14162
|
}
|
|
13983
14163
|
|
|
13984
|
-
// src/cli/tenant-input.ts
|
|
13985
|
-
import { input, password } from "@inquirer/prompts";
|
|
13986
|
-
async function promptTenantValue(message, secret = false) {
|
|
13987
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
13988
|
-
throw new Error(
|
|
13989
|
-
"Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
|
|
13990
|
-
);
|
|
13991
|
-
const prompt = secret ? password : input;
|
|
13992
|
-
return prompt(
|
|
13993
|
-
{
|
|
13994
|
-
message,
|
|
13995
|
-
validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
|
|
13996
|
-
},
|
|
13997
|
-
{ input: process.stdin, output: process.stderr }
|
|
13998
|
-
);
|
|
13999
|
-
}
|
|
14000
|
-
async function readTenantStdin() {
|
|
14001
|
-
if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
|
|
14002
|
-
const chunks = [];
|
|
14003
|
-
let size = 0;
|
|
14004
|
-
for await (const chunk of process.stdin) {
|
|
14005
|
-
const buffer = Buffer.from(chunk);
|
|
14006
|
-
size += buffer.length;
|
|
14007
|
-
if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
|
|
14008
|
-
chunks.push(buffer);
|
|
14009
|
-
}
|
|
14010
|
-
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
14011
|
-
if (!value.trim() || /[\r\n\0]/.test(value))
|
|
14012
|
-
throw new Error("Provide one nonempty configuration value on stdin");
|
|
14013
|
-
return value;
|
|
14014
|
-
}
|
|
14015
|
-
|
|
14016
14164
|
// src/cli/commands/tenant.ts
|
|
14017
14165
|
function tenantInputFailure(error) {
|
|
14018
14166
|
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
@@ -14022,22 +14170,43 @@ function tenantInputFailure(error) {
|
|
|
14022
14170
|
}
|
|
14023
14171
|
fail(error);
|
|
14024
14172
|
}
|
|
14025
|
-
var
|
|
14173
|
+
var COLUMNS = [
|
|
14026
14174
|
{ key: "name", label: "Tenant" },
|
|
14027
14175
|
{ key: "active", label: "Selected" },
|
|
14028
14176
|
{ key: "tsgId", label: "TSG ID" },
|
|
14029
14177
|
{ key: "configPath", label: "Config file" }
|
|
14030
14178
|
];
|
|
14179
|
+
var KEY_VALUE_COLUMNS = [
|
|
14180
|
+
{ key: "key", label: "Key" },
|
|
14181
|
+
{ key: "value", label: "Value" }
|
|
14182
|
+
];
|
|
14183
|
+
function resolveEntry(name) {
|
|
14184
|
+
const store = readTenantStore();
|
|
14185
|
+
const selected = name ?? store.active;
|
|
14186
|
+
if (!selected)
|
|
14187
|
+
throw new Error(
|
|
14188
|
+
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"
|
|
14189
|
+
);
|
|
14190
|
+
const entry = store.tenants.find((value) => value.name === selected);
|
|
14191
|
+
if (!entry) throw new Error("Tenant not found");
|
|
14192
|
+
return entry;
|
|
14193
|
+
}
|
|
14194
|
+
function redact(key, value) {
|
|
14195
|
+
return isTenantSecret(key) && value ? "[REDACTED]" : value ?? "";
|
|
14196
|
+
}
|
|
14031
14197
|
function registerTenantCommand(program) {
|
|
14032
|
-
const tenant = program.command("tenant").description("Create, configure and select
|
|
14198
|
+
const tenant = program.command("tenant").description("Create, configure and select tenants (the only configuration source)").addHelpText(
|
|
14033
14199
|
"after",
|
|
14034
14200
|
examples(
|
|
14035
14201
|
"airs tenant create development",
|
|
14202
|
+
"airs tenant switch development",
|
|
14036
14203
|
"airs tenant set development defaultOutput yaml",
|
|
14204
|
+
"airs tenant set development airsApiKey",
|
|
14205
|
+
"airs tenant unset development defaultOutput",
|
|
14206
|
+
"airs tenant get development mgmtTsgId",
|
|
14037
14207
|
"airs tenant create production --config /secure/production.json",
|
|
14038
|
-
"airs tenant switch production",
|
|
14039
14208
|
"airs tenant read",
|
|
14040
|
-
"airs tenant
|
|
14209
|
+
"airs tenant path"
|
|
14041
14210
|
)
|
|
14042
14211
|
);
|
|
14043
14212
|
tenant.command("create <name>").description(
|
|
@@ -14063,7 +14232,7 @@ function registerTenantCommand(program) {
|
|
|
14063
14232
|
}
|
|
14064
14233
|
}
|
|
14065
14234
|
);
|
|
14066
|
-
tenant.command("set <name> <key> [value]").description("Update one
|
|
14235
|
+
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
14236
|
async (name, key, value, opts) => {
|
|
14068
14237
|
try {
|
|
14069
14238
|
validateTenantSettingKey(key);
|
|
@@ -14083,14 +14252,32 @@ function registerTenantCommand(program) {
|
|
|
14083
14252
|
}
|
|
14084
14253
|
}
|
|
14085
14254
|
);
|
|
14086
|
-
tenant.command("
|
|
14087
|
-
|
|
14088
|
-
|
|
14255
|
+
tenant.command("unset <name> <key>").description("Remove one tenant setting so the default applies; credentials cannot be cleared").action(async (name, key) => {
|
|
14256
|
+
try {
|
|
14257
|
+
const removed = await unsetTenantSetting(name, key);
|
|
14258
|
+
if (removed) ui.success(`Removed ${key} from tenant ${name}; selection unchanged.`);
|
|
14259
|
+
else ui.info(`${key} is not set for tenant ${name} \u2014 nothing to do`);
|
|
14260
|
+
} catch (error) {
|
|
14261
|
+
fail(error);
|
|
14262
|
+
}
|
|
14263
|
+
});
|
|
14264
|
+
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) => {
|
|
14265
|
+
try {
|
|
14266
|
+
validateTenantSettingKey(key);
|
|
14267
|
+
const format = await resolveOutput(get, opts, { ignoreConfig: true });
|
|
14268
|
+
const entry = resolveEntry(name);
|
|
14269
|
+
const config = readTenantConfig(entry.configPath, entry.tsgId);
|
|
14270
|
+
const value = redact(key, config[key]);
|
|
14271
|
+
if (format === "pretty") console.log(String(value));
|
|
14272
|
+
else console.log(formatOutput([{ key, value }], KEY_VALUE_COLUMNS, format));
|
|
14273
|
+
} catch (error) {
|
|
14274
|
+
fail(error);
|
|
14275
|
+
}
|
|
14276
|
+
});
|
|
14277
|
+
tenant.command("switch <name>").description("Persist the selected tenant for subsequent commands").action(async (name) => {
|
|
14089
14278
|
try {
|
|
14090
14279
|
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
|
-
);
|
|
14280
|
+
ui.success(`Selected ${entry.name} (TSG ${entry.tsgId})`);
|
|
14094
14281
|
} catch (error) {
|
|
14095
14282
|
fail(error);
|
|
14096
14283
|
}
|
|
@@ -14099,65 +14286,63 @@ function registerTenantCommand(program) {
|
|
|
14099
14286
|
try {
|
|
14100
14287
|
const format = await resolveOutput(list, opts, { ignoreConfig: true });
|
|
14101
14288
|
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));
|
|
14289
|
+
if (store.tenants.length === 0 && format === "pretty") {
|
|
14290
|
+
ui.emptyList("tenants");
|
|
14291
|
+
ui.status("Run 'airs tenant create <name>' to register one.");
|
|
14292
|
+
return;
|
|
14293
|
+
}
|
|
14294
|
+
if (store.active === null && store.tenants.length)
|
|
14295
|
+
ui.status("No tenant selected; run 'airs tenant switch <name>'.");
|
|
14296
|
+
const rows = store.tenants.map((entry) => ({
|
|
14297
|
+
...entry,
|
|
14298
|
+
active: entry.name === store.active
|
|
14299
|
+
}));
|
|
14300
|
+
console.log(formatOutput(rows, COLUMNS, format === "pretty" ? "table" : format));
|
|
14115
14301
|
} catch (error) {
|
|
14116
14302
|
fail(error);
|
|
14117
14303
|
}
|
|
14118
14304
|
});
|
|
14119
|
-
const read = tenant.command("read [name]").description(
|
|
14305
|
+
const read = tenant.command("read [name]").description(
|
|
14306
|
+
"Read a tenant config with all credential values redacted; defaults to the selected tenant"
|
|
14307
|
+
).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
|
|
14120
14308
|
try {
|
|
14121
14309
|
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);
|
|
14310
|
+
const entry = resolveEntry(name);
|
|
14311
|
+
const config = readTenantConfig(entry.configPath, entry.tsgId);
|
|
14128
14312
|
const rows = Object.entries(config).map(([key, value]) => ({
|
|
14129
14313
|
key,
|
|
14130
|
-
value:
|
|
14314
|
+
value: redact(key, value)
|
|
14131
14315
|
}));
|
|
14132
|
-
ui.status(`Tenant ${
|
|
14133
|
-
console.log(
|
|
14134
|
-
formatOutput(
|
|
14135
|
-
rows,
|
|
14136
|
-
[
|
|
14137
|
-
{ key: "key", label: "Key" },
|
|
14138
|
-
{ key: "value", label: "Value" }
|
|
14139
|
-
],
|
|
14140
|
-
format === "pretty" ? "table" : format
|
|
14141
|
-
)
|
|
14142
|
-
);
|
|
14316
|
+
ui.status(`Tenant ${entry.name} (TSG ${entry.tsgId}): ${entry.configPath}`);
|
|
14317
|
+
console.log(formatOutput(rows, KEY_VALUE_COLUMNS, format === "pretty" ? "table" : format));
|
|
14143
14318
|
} catch (error) {
|
|
14144
14319
|
fail(error);
|
|
14145
14320
|
}
|
|
14146
14321
|
});
|
|
14147
|
-
tenant.command("
|
|
14322
|
+
tenant.command("path [name]").description("Print the config file path of a tenant; defaults to the selected tenant").action((name) => {
|
|
14323
|
+
try {
|
|
14324
|
+
console.log(resolveEntry(name).configPath);
|
|
14325
|
+
} catch (error) {
|
|
14326
|
+
fail(error);
|
|
14327
|
+
}
|
|
14328
|
+
});
|
|
14329
|
+
tenant.command("delete <name>").description(
|
|
14330
|
+
"Unregister a tenant; retain its source config file and clear the selection if it was selected"
|
|
14331
|
+
).option("--force", "Skip interactive confirmation").action(async (name, opts) => {
|
|
14148
14332
|
try {
|
|
14149
14333
|
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
14334
|
if (!store.tenants.some((entry) => entry.name === name))
|
|
14153
14335
|
throw new Error("Tenant not found");
|
|
14336
|
+
const selected = store.active === name;
|
|
14154
14337
|
await confirmOrAbort(
|
|
14155
|
-
`Unregister tenant ${name}? Its config file will be retained.`,
|
|
14338
|
+
`Unregister tenant ${name}?${selected ? " It is the selected tenant; no tenant will be selected afterwards." : ""} Its config file will be retained.`,
|
|
14156
14339
|
Boolean(opts.force),
|
|
14157
14340
|
{ action: `unregister tenant ${name}` }
|
|
14158
14341
|
);
|
|
14159
|
-
await deleteTenant(name);
|
|
14160
|
-
ui.success(
|
|
14342
|
+
const { selectionCleared } = await deleteTenant(name);
|
|
14343
|
+
ui.success(
|
|
14344
|
+
`Unregistered ${name}; source config file retained${selectionCleared ? "; no tenant is selected" : ""}`
|
|
14345
|
+
);
|
|
14161
14346
|
} catch (error) {
|
|
14162
14347
|
fail(error);
|
|
14163
14348
|
}
|
|
@@ -14218,7 +14403,7 @@ function applySortedHelp(cmd) {
|
|
|
14218
14403
|
for (const sub of cmd.commands) applySortedHelp(sub);
|
|
14219
14404
|
}
|
|
14220
14405
|
function buildProgram() {
|
|
14221
|
-
const here =
|
|
14406
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
14222
14407
|
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
14223
14408
|
const program = new Command();
|
|
14224
14409
|
let dlpDebugBodyEnvironment;
|
|
@@ -14299,7 +14484,6 @@ function buildProgram() {
|
|
|
14299
14484
|
registerModelSecurityCommand(program);
|
|
14300
14485
|
registerAgentGuardCommand(program);
|
|
14301
14486
|
registerAiGatewayCommand(program);
|
|
14302
|
-
registerConfigCommand(program);
|
|
14303
14487
|
registerTenantCommand(program);
|
|
14304
14488
|
registerDoctorCommand(program);
|
|
14305
14489
|
registerCompletionCommand(program);
|