@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 CHANGED
@@ -24,10 +24,9 @@
24
24
  - **Model Security** — ML model supply chain scanning with security groups, rules, and violation tracking
25
25
  - **Unified automation output** — resource reads support `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout; environment deliverables use HTML or Markdown
26
26
  - **Complete pagination** — consistent `--limit`, `--offset`, and `--all` traversal with a configurable safety cap
27
- - **`airs doctor`** — one-command diagnostics for environment, credentials, and API connectivity
28
- - **Tenant configuration** — `airs tenant create|set|switch|list|read|delete` supports guided setup with hidden secret prompts, individual setting updates, and existing-file registration without changing read-only mounts
27
+ - **`airs doctor`** — one-command diagnostics for the selected tenant, its credentials, and API connectivity
28
+ - **Tenant configuration** — `airs tenant create|switch|set|unset|get|list|read|path|delete` is the only configuration surface: guided setup with hidden secret prompts, individual setting updates, and existing-file registration without changing read-only mounts
29
29
  - **Profile migration** — `airs runtime profiles backup|restore` exports private JSON/YAML, previews cross-tenant restores, remaps topics and explicit DLP dependencies, and verifies restored policies ([guide](https://cdot65.github.io/prisma-airs-cli/runtime/profile-transfer/))
30
- - **`airs config`** — manage the selected config file from the CLI (`list`, `get`, `set`, `unset`, `path`)
31
30
 
32
31
  ## Install
33
32
 
@@ -41,8 +40,9 @@ Requires **Node.js 20.17+, 22.13+, or 24+** (exact engine range: `^20.17.0 || ^2
41
40
  ## Quick Start
42
41
 
43
42
  ```bash
44
- # Configure credentials
45
- cp .env.example .env # add your API keys
43
+ # Configure credentials (prompts for TSG ID, client ID, and a hidden secret)
44
+ airs tenant create dev
45
+ airs tenant switch dev
46
46
 
47
47
  # Check your setup
48
48
  airs doctor
@@ -101,13 +101,13 @@ Resource read commands share one contract (environment report files have their o
101
101
  - JSON/YAML lists are bare arrays of complete normalized records; detail reads are complete objects.
102
102
  - Table, Markdown, and CSV are stable human-oriented projections. CSV uses RFC 4180 quoting.
103
103
  - Data is written to stdout; status, paging hints, warnings, and errors are written to stderr.
104
- - Output precedence is command `--output`, global `--output`, `defaultOutput`/`PANW_CLI_OUTPUT`, then `pretty`.
104
+ - Output precedence is command `--output`, global `--output`, `defaultOutput`, then `pretty`.
105
105
  - Paginated lists use `--limit`, `--offset`, and `--all`. Complete traversal is capped at 10,000 records by default; change it with `--max`, or use `--max 0` for no cap.
106
106
  - Profile and topic lists return only the latest revision by default. Use `--all-versions` or `--revision` when historical revisions are needed.
107
107
 
108
108
  ```bash
109
109
  airs --output json runtime profiles list --all | jq '.[].profileName'
110
- PANW_CLI_OUTPUT=yaml airs runtime topics get "My Topic"
110
+ airs --output yaml runtime topics get "My Topic"
111
111
  airs model-security scans list --all --max 25000 --output csv > scans.csv
112
112
  ```
113
113
 
@@ -125,7 +125,7 @@ The full guides, complete CLI reference, configuration, and architecture live on
125
125
 
126
126
  ## Configuration
127
127
 
128
- Credentials come from environment variables or `~/.prisma-airs/config.json`. At minimum: `PANW_AI_SEC_API_KEY` (scanning) and `PANW_MGMT_CLIENT_ID` / `PANW_MGMT_CLIENT_SECRET` / `PANW_MGMT_TSG_ID` (management). Set `defaultOutput` in the config file or `PANW_CLI_OUTPUT` in the environment to choose a default read format. See [`.env.example`](.env.example) and the [configuration guide](https://cdot65.github.io/prisma-airs-cli/getting-started/configuration/) for the full list.
128
+ Configuration lives only in tenant files managed by `airs tenant`; environment variables are not read. Every management-plane product (Management, DLP, Red Team, Model Security, AgentGuard, AI Gateway) authenticates with the tenant's single SCM OAuth credential set (`mgmtClientId`, `mgmtClientSecret`, `mgmtTsgId`); scanning adds `airsApiKey`. Set `defaultOutput` with `airs tenant set <name> defaultOutput json` to choose a default read format. See the [configuration guide](https://cdot65.github.io/prisma-airs-cli/getting-started/configuration/) for the full list.
129
129
 
130
130
  ## License
131
131
 
@@ -2239,38 +2239,31 @@ import { join as join2 } from "path";
2239
2239
  // src/config/schema.ts
2240
2240
  import { z } from "zod";
2241
2241
  var ConfigSchema = z.object({
2242
- // AIRS Scanner (runtime scan API)
2242
+ // AIRS Scanner (runtime scan API) — separate key-based auth
2243
2243
  airsApiKey: z.string().optional(),
2244
2244
  airsApiToken: z.string().optional(),
2245
2245
  airsApiEndpoint: z.string().optional(),
2246
2246
  airsNumRetries: z.coerce.number().int().min(0).max(5).optional(),
2247
- // AIRS Management (SDK v2 — OAuth2 client credentials)
2247
+ // Shared SCM OAuth2 client credentials
2248
2248
  mgmtClientId: z.string().optional(),
2249
2249
  mgmtClientSecret: z.string().optional(),
2250
2250
  mgmtTsgId: z.string().optional(),
2251
2251
  mgmtEndpoint: z.string().optional(),
2252
2252
  mgmtDashboardEndpoint: z.string().url().optional(),
2253
2253
  mgmtTokenEndpoint: z.string().optional(),
2254
+ // Product base-URL overrides (authentication always comes from mgmt*)
2254
2255
  dlpEndpoint: z.string().optional(),
2255
- // Red Team (endpoints only; creds shared with mgmt*)
2256
2256
  redTeamDataEndpoint: z.string().optional(),
2257
2257
  redTeamMgmtEndpoint: z.string().optional(),
2258
- redTeamTokenEndpoint: z.string().optional(),
2259
2258
  redTeamNetworkBrokerEndpoint: z.string().optional(),
2260
- // Model Security (endpoints only; creds shared with mgmt*)
2261
2259
  modelSecDataEndpoint: z.string().optional(),
2262
2260
  modelSecMgmtEndpoint: z.string().optional(),
2263
- modelSecTokenEndpoint: z.string().optional(),
2264
2261
  agentGuardDataEndpoint: z.string().url().optional(),
2265
2262
  agentGuardMgmtEndpoint: z.string().url().optional(),
2266
- agentGuardTokenEndpoint: z.string().url().optional(),
2267
- // AI Gateway (endpoints only; creds shared with mgmt*)
2268
2263
  aiGwDataEndpoint: z.string().optional(),
2269
2264
  aiGwAdminEndpoint: z.string().optional(),
2270
- aiGwTokenEndpoint: z.string().optional(),
2271
- // SCM IAM (workspace scopes); creds shared with mgmt*
2272
2265
  iamEndpoint: z.string().optional(),
2273
- // Runtime inference has separate key authentication, never management OAuth.
2266
+ // AI Gateway runtime inference authenticates with a workspace key, never SCM OAuth.
2274
2267
  aiGwInferenceEndpoint: z.string().optional(),
2275
2268
  aiGwInferenceApiKey: z.string().optional(),
2276
2269
  aiGwInferenceModel: z.string().optional(),
@@ -2281,6 +2274,13 @@ var ConfigSchema = z.object({
2281
2274
  // Persistence
2282
2275
  dataDir: z.string().default("~/.prisma-airs/runs")
2283
2276
  });
2277
+ var CONFIG_KEYS = Object.keys(ConfigSchema.shape);
2278
+ var RETIRED_CONFIG_KEYS = [
2279
+ "redTeamTokenEndpoint",
2280
+ "modelSecTokenEndpoint",
2281
+ "agentGuardTokenEndpoint",
2282
+ "aiGwTokenEndpoint"
2283
+ ];
2284
2284
 
2285
2285
  // src/config/tenants.ts
2286
2286
  import { randomUUID } from "crypto";
@@ -2289,7 +2289,7 @@ import { mkdir, open, rename, unlink } from "fs/promises";
2289
2289
  import { homedir } from "os";
2290
2290
  import { dirname, isAbsolute, join, resolve } from "path";
2291
2291
  import { z as z2 } from "zod";
2292
- var NameSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/).refine((v) => v !== "default");
2292
+ var NameSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
2293
2293
  var EntrySchema = z2.object({
2294
2294
  name: NameSchema,
2295
2295
  configPath: z2.string().refine(isAbsolute),
@@ -2314,9 +2314,6 @@ function tenantStorePath() {
2314
2314
  function expandConfigPath(path2) {
2315
2315
  return path2 === "~" ? homedir() : path2.startsWith("~/") ? join(homedir(), path2.slice(2)) : path2;
2316
2316
  }
2317
- function defaultTenantConfigPath() {
2318
- return join(homedir(), ".prisma-airs", "config.json");
2319
- }
2320
2317
  function missing(error) {
2321
2318
  return error?.code === "ENOENT";
2322
2319
  }
@@ -2333,10 +2330,6 @@ function readTenantStore() {
2333
2330
  );
2334
2331
  }
2335
2332
  }
2336
- function selectedTenant() {
2337
- const store = readTenantStore();
2338
- return store.tenants.find((entry) => entry.name === store.active);
2339
- }
2340
2333
  function readTenantConfig(path2, expectedTsgId) {
2341
2334
  return ConfigSchema.parse(readTenantConfigFile(path2, expectedTsgId));
2342
2335
  }
@@ -2358,15 +2351,6 @@ function readTenantConfigFile(path2, expectedTsgId) {
2358
2351
  );
2359
2352
  return parsed;
2360
2353
  }
2361
- function assertTenantEnvironment() {
2362
- const conflicts = Object.keys(process.env).filter(
2363
- (key) => /^PANW_(?:MGMT_|MODEL_SEC_|RED_TEAM_|AGENT_GUARD_|AI_GW_|DLP_|AI_SEC_API_)/.test(key) && Boolean(process.env[key])
2364
- );
2365
- if (conflicts.length)
2366
- throw new Error(
2367
- `Named tenant selection conflicts with environment overrides: ${conflicts.sort().join(", ")}. Unset them or switch to default.`
2368
- );
2369
- }
2370
2354
  async function changeStore(change) {
2371
2355
  const path2 = tenantStorePath();
2372
2356
  await mkdir(dirname(path2), { recursive: true, mode: 448 });
@@ -2407,7 +2391,7 @@ async function changeStore(change) {
2407
2391
  function validateTenantName(name) {
2408
2392
  if (!NameSchema.safeParse(name).success)
2409
2393
  throw new Error(
2410
- "Tenant name must be 1\u201364 letters, digits, hyphens or underscores, start with a letter/digit, and not be default"
2394
+ "Tenant name must be 1\u201364 letters, digits, hyphens or underscores and start with a letter/digit"
2411
2395
  );
2412
2396
  }
2413
2397
  async function createTenant(name, configPath) {
@@ -2433,15 +2417,6 @@ async function createTenant(name, configPath) {
2433
2417
  async function switchTenant(name) {
2434
2418
  let selected;
2435
2419
  await changeStore((store) => {
2436
- if (name === "default") {
2437
- store.active = null;
2438
- return;
2439
- }
2440
- if (process.env.PRISMA_AIRS_CONFIG_PATH)
2441
- throw new Error(
2442
- "Unset PRISMA_AIRS_CONFIG_PATH before switching tenants; it overrides named selection"
2443
- );
2444
- assertTenantEnvironment();
2445
2420
  selected = store.tenants.find((entry) => entry.name === name);
2446
2421
  if (!selected) throw new Error("Tenant not found");
2447
2422
  readTenantConfig(selected.configPath, selected.tsgId);
@@ -2450,56 +2425,22 @@ async function switchTenant(name) {
2450
2425
  return selected;
2451
2426
  }
2452
2427
  async function deleteTenant(name) {
2428
+ let selectionCleared = false;
2453
2429
  await changeStore((store) => {
2454
- if (name === "default") throw new Error("The default tenant cannot be deleted");
2455
- if (store.active === name)
2456
- throw new Error("Switch to another tenant or default before deleting the active tenant");
2457
2430
  if (!store.tenants.some((entry) => entry.name === name)) throw new Error("Tenant not found");
2458
2431
  store.tenants = store.tenants.filter((entry) => entry.name !== name);
2432
+ if (store.active === name) {
2433
+ store.active = null;
2434
+ selectionCleared = true;
2435
+ }
2459
2436
  });
2437
+ return { selectionCleared };
2460
2438
  }
2461
2439
 
2462
2440
  // src/config/loader.ts
2463
2441
  function expandHome(p) {
2464
2442
  return p.startsWith("~") ? join2(homedir2(), p.slice(1)) : p;
2465
2443
  }
2466
- function fromEnv() {
2467
- const env = process.env;
2468
- return {
2469
- airsApiKey: env.PANW_AI_SEC_API_KEY,
2470
- airsApiToken: env.PANW_AI_SEC_API_TOKEN,
2471
- airsApiEndpoint: env.PANW_AI_SEC_API_ENDPOINT,
2472
- airsNumRetries: env.PANW_AI_SEC_NUM_RETRIES,
2473
- mgmtClientId: env.PANW_MGMT_CLIENT_ID,
2474
- mgmtClientSecret: env.PANW_MGMT_CLIENT_SECRET,
2475
- mgmtTsgId: env.PANW_MGMT_TSG_ID,
2476
- mgmtEndpoint: env.PANW_MGMT_ENDPOINT,
2477
- mgmtDashboardEndpoint: env.PANW_MGMT_DASHBOARD_ENDPOINT,
2478
- mgmtTokenEndpoint: env.PANW_MGMT_TOKEN_ENDPOINT,
2479
- dlpEndpoint: env.PANW_DLP_ENDPOINT,
2480
- redTeamDataEndpoint: env.PANW_RED_TEAM_DATA_ENDPOINT,
2481
- redTeamMgmtEndpoint: env.PANW_RED_TEAM_MGMT_ENDPOINT,
2482
- redTeamTokenEndpoint: env.PANW_RED_TEAM_TOKEN_ENDPOINT,
2483
- redTeamNetworkBrokerEndpoint: env.PANW_RED_TEAM_NETWORK_BROKER_ENDPOINT,
2484
- modelSecDataEndpoint: env.PANW_MODEL_SEC_DATA_ENDPOINT,
2485
- modelSecMgmtEndpoint: env.PANW_MODEL_SEC_MGMT_ENDPOINT,
2486
- modelSecTokenEndpoint: env.PANW_MODEL_SEC_TOKEN_ENDPOINT,
2487
- agentGuardDataEndpoint: env.PANW_AGENT_GUARD_DATA_ENDPOINT,
2488
- agentGuardMgmtEndpoint: env.PANW_AGENT_GUARD_MGMT_ENDPOINT,
2489
- agentGuardTokenEndpoint: env.PANW_AGENT_GUARD_TOKEN_ENDPOINT,
2490
- aiGwDataEndpoint: env.PANW_AI_GW_DATA_ENDPOINT,
2491
- aiGwAdminEndpoint: env.PANW_AI_GW_ADMIN_ENDPOINT,
2492
- aiGwTokenEndpoint: env.PANW_AI_GW_TOKEN_ENDPOINT,
2493
- iamEndpoint: env.PANW_IAM_ENDPOINT,
2494
- aiGwInferenceEndpoint: env.PANW_AI_GW_INFERENCE_ENDPOINT,
2495
- aiGwInferenceApiKey: env.PANW_AI_GW_INFERENCE_API_KEY,
2496
- aiGwInferenceModel: env.PANW_AI_GW_INFERENCE_MODEL,
2497
- aiGwEmbeddingModel: env.PANW_AI_GW_EMBEDDING_MODEL,
2498
- scanConcurrency: env.SCAN_CONCURRENCY,
2499
- defaultOutput: env.PANW_CLI_OUTPUT,
2500
- dataDir: env.DATA_DIR
2501
- };
2502
- }
2503
2444
  async function fromFile(path2) {
2504
2445
  try {
2505
2446
  const data = await readFile(path2, "utf-8");
@@ -2511,45 +2452,43 @@ async function fromFile(path2) {
2511
2452
  function stripUndefined(obj) {
2512
2453
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0 && v !== ""));
2513
2454
  }
2514
- function resolveConfigFilePath(configFilePath) {
2515
- return resolveConfigSource(configFilePath).path;
2455
+ function noTenantSelectedMessage(registered) {
2456
+ return registered.length ? `No tenant selected. Run 'airs tenant switch <name>' (registered: ${registered.join(", ")})` : "No tenant selected. Run 'airs tenant create <name>' and then 'airs tenant switch <name>'";
2516
2457
  }
2517
- function resolveConfigSource(configFilePath) {
2518
- if (configFilePath) return { path: expandHome(configFilePath) };
2519
- const envPath = process.env.PRISMA_AIRS_CONFIG_PATH;
2520
- if (envPath) return { path: expandHome(envPath) };
2521
- const tenant = selectedTenant();
2522
- return { path: tenant?.configPath ?? join2(homedir2(), ".prisma-airs", "config.json"), tenant };
2458
+ var NoTenantSelectedError = class extends Error {
2459
+ constructor(registered) {
2460
+ super(noTenantSelectedMessage(registered));
2461
+ this.name = "NoTenantSelectedError";
2462
+ }
2463
+ };
2464
+ function resolveConfigContext(configFilePath) {
2465
+ const registryPath = tenantStorePath();
2466
+ if (configFilePath) {
2467
+ return { path: expandHome(configFilePath), selection: "explicit", registryPath };
2468
+ }
2469
+ const store = readTenantStore();
2470
+ const tenant = store.tenants.find((entry) => entry.name === store.active);
2471
+ if (tenant) return { path: tenant.configPath, selection: "tenant", tenant, registryPath };
2472
+ return { selection: "none", registryPath, registered: store.tenants.map((entry) => entry.name) };
2523
2473
  }
2524
2474
  async function readConfigSource(configFilePath) {
2525
- const { path: path2, tenant } = resolveConfigSource(configFilePath);
2526
- if (!tenant) return fromFile(path2);
2527
- assertTenantEnvironment();
2528
- return readTenantConfigFile(path2, tenant.tsgId);
2475
+ const context = resolveConfigContext(configFilePath);
2476
+ if (context.selection === "none") throw new NoTenantSelectedError(context.registered);
2477
+ if (context.selection === "explicit") return fromFile(context.path);
2478
+ return readTenantConfigFile(context.path, context.tenant.tsgId);
2529
2479
  }
2530
2480
  async function loadConfig(cliOverrides = {}, configFilePath) {
2531
2481
  const fileConfig = await readConfigSource(configFilePath);
2532
- const envConfig = fromEnv();
2533
- const merged = {
2534
- ...stripUndefined(fileConfig),
2535
- ...stripUndefined(envConfig),
2536
- ...stripUndefined(cliOverrides)
2537
- };
2482
+ const merged = { ...stripUndefined(fileConfig), ...stripUndefined(cliOverrides) };
2538
2483
  const config = ConfigSchema.parse(merged);
2539
- return {
2540
- ...config,
2541
- dataDir: expandHome(config.dataDir)
2542
- };
2484
+ return { ...config, dataDir: expandHome(config.dataDir) };
2543
2485
  }
2544
2486
  async function inspectConfig(configFilePath) {
2545
2487
  const fileConfig = stripUndefined(await readConfigSource(configFilePath));
2546
- const envConfig = stripUndefined(fromEnv());
2547
- const merged = { ...fileConfig, ...envConfig };
2548
- const config = ConfigSchema.parse(merged);
2488
+ const config = ConfigSchema.parse(fileConfig);
2549
2489
  const entries = {};
2550
2490
  for (const key of Object.keys(ConfigSchema.shape)) {
2551
- const source2 = key in envConfig ? "env" : key in fileConfig ? "file" : "default";
2552
- entries[key] = { value: config[key], source: source2 };
2491
+ entries[key] = { value: config[key], source: key in fileConfig ? "file" : "default" };
2553
2492
  }
2554
2493
  return entries;
2555
2494
  }
@@ -4192,9 +4131,8 @@ export {
4192
4131
  readBackupFile,
4193
4132
  readBackupDir,
4194
4133
  ConfigSchema,
4134
+ RETIRED_CONFIG_KEYS,
4195
4135
  tenantStorePath,
4196
- expandConfigPath,
4197
- defaultTenantConfigPath,
4198
4136
  readTenantStore,
4199
4137
  readTenantConfig,
4200
4138
  readTenantConfigFile,
@@ -4202,7 +4140,7 @@ export {
4202
4140
  createTenant,
4203
4141
  switchTenant,
4204
4142
  deleteTenant,
4205
- resolveConfigFilePath,
4143
+ resolveConfigContext,
4206
4144
  loadConfig,
4207
4145
  inspectConfig,
4208
4146
  validateName,