@cdot65/prisma-airs-cli 5.10.0 → 5.11.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
@@ -20,7 +20,7 @@
20
20
  - **SCM dashboard and sessions** — `runtime dashboard` and `runtime sessions` expose application activity, daily trends, checked pagination and explicit session-to-content drill-down. Legacy `scan-logs query` is **broken/under refactor** and exits with migration guidance; see the [session reference](https://cdot65.github.io/prisma-airs-cli/cli/runtime/sessions/)
21
21
  - **Guardrail Optimization** — atomic CLI commands (`create`, `apply`, `eval`, `revert`) for custom topic guardrails, designed for autonomous agent loops (see [`AGENTS.md`](AGENTS.md))
22
22
  - **AI Red Teaming** — adversarial scanning with static, dynamic, and custom prompt set attack modes
23
- - **[AI Gateway](https://cdot65.github.io/prisma-airs-cli/cli/aigateway/resources/)** — workspaces, configs, guardrails, providers, API keys, integrations, MCP, deployments, plugins, audit logs, and telemetry
23
+ - **[AI Gateway](https://cdot65.github.io/prisma-airs-cli/cli/aigateway/resources/)** — workspaces (scope-first provisioning), IAM scopes, configs, guardrails, providers, API keys, integrations, MCP, deployments, plugins, audit logs, and telemetry
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
@@ -2268,6 +2268,8 @@ var ConfigSchema = z.object({
2268
2268
  aiGwDataEndpoint: z.string().optional(),
2269
2269
  aiGwAdminEndpoint: z.string().optional(),
2270
2270
  aiGwTokenEndpoint: z.string().optional(),
2271
+ // SCM IAM (workspace scopes); creds shared with mgmt*
2272
+ iamEndpoint: z.string().optional(),
2271
2273
  // Runtime inference has separate key authentication, never management OAuth.
2272
2274
  aiGwInferenceEndpoint: z.string().optional(),
2273
2275
  aiGwInferenceApiKey: z.string().optional(),
@@ -2488,6 +2490,7 @@ function fromEnv() {
2488
2490
  aiGwDataEndpoint: env.PANW_AI_GW_DATA_ENDPOINT,
2489
2491
  aiGwAdminEndpoint: env.PANW_AI_GW_ADMIN_ENDPOINT,
2490
2492
  aiGwTokenEndpoint: env.PANW_AI_GW_TOKEN_ENDPOINT,
2493
+ iamEndpoint: env.PANW_IAM_ENDPOINT,
2491
2494
  aiGwInferenceEndpoint: env.PANW_AI_GW_INFERENCE_ENDPOINT,
2492
2495
  aiGwInferenceApiKey: env.PANW_AI_GW_INFERENCE_API_KEY,
2493
2496
  aiGwInferenceModel: env.PANW_AI_GW_INFERENCE_MODEL,
package/dist/cli/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  validateTopic,
41
41
  writeBackupFile,
42
42
  writeReportFile
43
- } from "../chunk-XQOICLSH.js";
43
+ } from "../chunk-37SN57XQ.js";
44
44
 
45
45
  // src/cli/index.ts
46
46
  import "dotenv/config";
@@ -126,6 +126,7 @@ function aiGatewayClientOptions(config) {
126
126
  tsgId: config.mgmtTsgId,
127
127
  dataEndpoint: config.aiGwDataEndpoint,
128
128
  adminEndpoint: config.aiGwAdminEndpoint,
129
+ iamEndpoint: config.iamEndpoint,
129
130
  tokenEndpoint: config.aiGwTokenEndpoint ?? config.mgmtTokenEndpoint
130
131
  };
131
132
  }
@@ -707,6 +708,62 @@ function renderWorkspaceDetail(workspace, format = "pretty") {
707
708
  }
708
709
  console.log();
709
710
  }
711
+ function scopeBindings(scope) {
712
+ return scope.resources.map((r) => `${r.resourceType}:${r.resourceId}`).join(", ");
713
+ }
714
+ function renderScopeList(scopes, format = "pretty") {
715
+ if (scopes.length === 0 && (format === "json" || format === "yaml")) {
716
+ console.log(format === "json" ? "[]" : yamlDump([]).trimEnd());
717
+ return;
718
+ }
719
+ if (scopes.length === 0) {
720
+ ui.emptyList("IAM scopes");
721
+ return;
722
+ }
723
+ if (format !== "pretty") {
724
+ const rows = scopes.map((s) => ({
725
+ name: s.name,
726
+ description: s.description,
727
+ resources: scopeBindings(s),
728
+ tsgId: s.tsgId
729
+ }));
730
+ console.log(
731
+ formatOutput(
732
+ rows,
733
+ [
734
+ { key: "name", label: "Name" },
735
+ { key: "description", label: "Description" },
736
+ { key: "resources", label: "Bound resources" },
737
+ { key: "tsgId", label: "TSG" }
738
+ ],
739
+ format
740
+ )
741
+ );
742
+ return;
743
+ }
744
+ ui.section("IAM Scopes:");
745
+ for (const s of scopes) {
746
+ const bindings = scopeBindings(s);
747
+ console.log(` ${s.name} ${bindings ? chalk4.dim(bindings) : chalk4.yellow("unbound")}`);
748
+ if (s.description) console.log(` ${chalk4.dim(s.description)}`);
749
+ console.log();
750
+ }
751
+ }
752
+ function renderScopeDetail(scope, format = "pretty") {
753
+ if (format !== "pretty") {
754
+ console.log(format === "json" ? JSON.stringify(scope, null, 2) : yamlDump(scope));
755
+ return;
756
+ }
757
+ ui.section("IAM Scope:");
758
+ ui.keyValue([
759
+ ["Name", scope.name],
760
+ ["Description", scope.description || chalk4.dim("(none)")],
761
+ ["TSG", scope.tsgId],
762
+ ["ID", scope.id],
763
+ ["Bound resources", scopeBindings(scope) || chalk4.yellow("none \u2014 unbound")]
764
+ ]);
765
+ console.log();
766
+ }
710
767
  function renderCostReport(report, format = "pretty") {
711
768
  if (format !== "pretty") {
712
769
  emitDetail(
@@ -3251,6 +3308,18 @@ function normalizeWorkspaceDetail(raw) {
3251
3308
  settings: raw.settings
3252
3309
  };
3253
3310
  }
3311
+ function normalizeScope(raw) {
3312
+ return {
3313
+ name: raw.name,
3314
+ description: raw.description,
3315
+ resources: raw.resources.map((r) => ({
3316
+ resourceType: r.resource_type,
3317
+ resourceId: r.resource_id
3318
+ })),
3319
+ tsgId: raw.tsg_id,
3320
+ id: raw.id
3321
+ };
3322
+ }
3254
3323
  function aiGatewayGrantHint(err) {
3255
3324
  const status = err?.status ?? err?.statusCode;
3256
3325
  if (status !== 403) return void 0;
@@ -3294,18 +3363,38 @@ var SdkAiGatewayService = class {
3294
3363
  }
3295
3364
  }
3296
3365
  async createWorkspace(request) {
3297
- const body = {
3298
- name: request.name,
3299
- scope_name: request.scopeName
3300
- };
3366
+ const body = { name: request.name };
3367
+ if (request.scopeName !== void 0) body.scope_name = request.scopeName;
3301
3368
  if (request.description !== void 0) body.description = request.description;
3302
3369
  if (request.icon !== void 0) body.icon = request.icon;
3303
3370
  if (request.defaults !== void 0) body.defaults = request.defaults;
3304
3371
  if (request.users !== void 0) body.users = request.users;
3305
3372
  if (request.usageLimits !== void 0) body.usage_limits = request.usageLimits;
3306
3373
  if (request.rateLimits !== void 0) body.rate_limits = request.rateLimits;
3307
- const created = await this.client.workspaces.create(body);
3308
- return this.refetchAfterWrite(created.id, created);
3374
+ const result = await this.client.workspaces.provision(
3375
+ body,
3376
+ request.existingScope ? { existingScope: true } : {}
3377
+ );
3378
+ const created = result.workspace;
3379
+ const workspace = await this.refetchAfterWrite(created.id, created);
3380
+ return { workspace, scope: normalizeScope(result.scope), scopeCreated: result.scopeCreated };
3381
+ }
3382
+ async listScopes() {
3383
+ const response = await this.client.iamScopes.list();
3384
+ return response.items.map(normalizeScope);
3385
+ }
3386
+ async getScope(name) {
3387
+ return normalizeScope(await this.client.iamScopes.get(name));
3388
+ }
3389
+ async createScope(request) {
3390
+ return normalizeScope(await this.client.iamScopes.create(request));
3391
+ }
3392
+ async bindScope(name, workspaceRef) {
3393
+ const slug = await this.resolveWorkspaceRef(workspaceRef, ["admin"], true);
3394
+ return normalizeScope(await this.client.iamScopes.bindWorkspace(name, slug));
3395
+ }
3396
+ async deleteScope(name) {
3397
+ await this.client.iamScopes.delete(name);
3309
3398
  }
3310
3399
  async updateWorkspace(workspaceRef, request) {
3311
3400
  const ref = await this.resolveWorkspaceRef(workspaceRef, ["admin"]);
@@ -5414,44 +5503,60 @@ function registerAiGatewayCommand(program) {
5414
5503
  failWithGrantHint(err);
5415
5504
  }
5416
5505
  });
5417
- workspace.command("create").description("Create a workspace (admin plane)").requiredOption("--name <name>", "Display name").requiredOption(
5506
+ const workspaceCreate = workspace.command("create").description("Create a workspace: IAM scope \u2192 workspace \u2192 bind scope (admin plane)").requiredOption("--name <name>", "Display name").option(
5418
5507
  "--scope-name <scope>",
5419
- "SCM role scope granting data-plane access (e.g. ws_production_bx7qw0) \u2014 not derived from --name"
5508
+ "IAM scope to create and bind (e.g. ws_production_bx7qw0). Default: ws_<name>_<suffix>"
5509
+ ).option(
5510
+ "--existing-scope",
5511
+ "Bind an IAM scope that already exists instead of creating one (requires --scope-name)"
5420
5512
  ).option("--description <text>", "Workspace description").option("--icon <icon>", "Workspace icon").option("--metadata <json>", "Sugar for defaults.metadata (flat string map)").option("--defaults <json>", "Workspace defaults object").option("--users <ids>", "Comma-separated user ids to seed the workspace with").option("--usage-limits <json>", "Usage-limit policies \u2014 a JSON ARRAY of policy objects").option("--rate-limits <json>", "Rate-limit policies \u2014 a JSON ARRAY of policy objects").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5421
5513
  "after",
5422
5514
  examples(
5515
+ 'airs aigateway workspaces create --name truffles --description "Online recipe generation application"',
5423
5516
  "airs aigateway workspaces create --name Production --scope-name ws_production_bx7qw0",
5424
- `airs aigateway workspaces create --name Production --scope-name ws_production_bx7qw0 --metadata '{"env":"production"}' --rate-limits '[{"type":"requests","unit":"rpm","value":100}]'`
5517
+ "airs aigateway workspaces create --name Staging --scope-name ws_staging_q1x8mz --existing-scope",
5518
+ `airs aigateway workspaces create --name Production --metadata '{"env":"production"}' --rate-limits '[{"type":"requests","unit":"rpm","value":100}]'`
5425
5519
  )
5426
5520
  ).action(async (opts) => {
5427
5521
  try {
5428
- const fmt = opts.output;
5522
+ const fmt = await resolveOutput(workspaceCreate, opts, {
5523
+ allowed: ["pretty", "json", "yaml"]
5524
+ });
5429
5525
  if (fmt === "pretty") renderAiGatewayHeader();
5430
- if (scopeNameLooksUnrelated(opts.name, opts.scopeName)) {
5526
+ if (opts.existingScope && !opts.scopeName) {
5527
+ throw new CliUsageError("--existing-scope requires --scope-name");
5528
+ }
5529
+ if (opts.scopeName && scopeNameLooksUnrelated(opts.name, opts.scopeName)) {
5431
5530
  ui.warn(
5432
- `--scope-name '${opts.scopeName}' shares no token with --name '${opts.name}'. A workspace created with a scope nobody holds will not appear in data-plane lists.`
5531
+ `--scope-name '${opts.scopeName}' shares no token with --name '${opts.name}'. Operators find workspaces by scope in SCM Access Management; an unrelated name is easy to lose.`
5433
5532
  );
5434
5533
  }
5435
5534
  const service = await createService();
5436
- const workspace2 = await service.createWorkspace({
5535
+ const { workspace: workspace2, scope, scopeCreated } = await service.createWorkspace({
5437
5536
  ...buildWorkspaceWriteRequest(opts),
5438
5537
  name: opts.name,
5439
- scopeName: opts.scopeName
5538
+ ...opts.scopeName !== void 0 ? { scopeName: opts.scopeName } : {},
5539
+ ...opts.existingScope ? { existingScope: true } : {}
5440
5540
  });
5441
5541
  ui.success(`Workspace created: ${workspace2.id}`);
5542
+ ui.status(
5543
+ `IAM scope ${scope.name} ${scopeCreated ? "created and " : ""}bound to workspace ${workspace2.slug}. Grant that scope to the service accounts that should reach the workspace on the data plane.`
5544
+ );
5442
5545
  renderWorkspaceDetail(workspace2, fmt);
5443
5546
  } catch (err) {
5444
5547
  failWithGrantHint(err);
5445
5548
  }
5446
5549
  });
5447
- workspace.command("update <ref>").description("Update a workspace (admin plane, partial patch)").option("--name <name>", "New display name").option("--description <text>", "New description").option("--icon <icon>", "New icon").option("--metadata <json>", "Sugar for defaults.metadata (flat string map)").option("--defaults <json>", "Workspace defaults object").option("--usage-limits <json>", "Usage-limit policies \u2014 a JSON ARRAY of policy objects").option("--rate-limits <json>", "Rate-limit policies \u2014 a JSON ARRAY of policy objects").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5550
+ const workspaceUpdate = workspace.command("update <ref>").description("Update a workspace (admin plane, partial patch)").option("--name <name>", "New display name").option("--description <text>", "New description").option("--icon <icon>", "New icon").option("--metadata <json>", "Sugar for defaults.metadata (flat string map)").option("--defaults <json>", "Workspace defaults object").option("--usage-limits <json>", "Usage-limit policies \u2014 a JSON ARRAY of policy objects").option("--rate-limits <json>", "Rate-limit policies \u2014 a JSON ARRAY of policy objects").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5448
5551
  "after",
5449
5552
  examples(
5450
5553
  `airs aigateway workspaces update ws-produc-985697 --description 'Production workloads, us-east'`
5451
5554
  )
5452
5555
  ).action(async (ref, opts) => {
5453
5556
  try {
5454
- const fmt = opts.output;
5557
+ const fmt = await resolveOutput(workspaceUpdate, opts, {
5558
+ allowed: ["pretty", "json", "yaml"]
5559
+ });
5455
5560
  if (fmt === "pretty") renderAiGatewayHeader();
5456
5561
  const request = buildWorkspaceWriteRequest(opts);
5457
5562
  if (Object.keys(request).length === 0) {
@@ -5492,6 +5597,83 @@ function registerAiGatewayCommand(program) {
5492
5597
  };
5493
5598
  workspace.command("archive <ref>").description("Archive a workspace (soft delete \u2014 there is no hard delete)").option("--force", "Skip confirmation prompt").addHelpText("after", examples("airs aigateway workspaces archive ws-produc-985697 --force")).action((ref, opts) => archiveWorkspace(ref, opts, false));
5494
5599
  workspace.command("delete <ref>", { hidden: true }).description("Deprecated compatibility command for archive").option("--force", "Skip confirmation prompt").action((ref, opts) => archiveWorkspace(ref, opts, true));
5600
+ const scopes = aigateway.command("scopes").description("Manage SCM IAM scopes \u2014 the objects a workspace scope_name points at (/iam/v1)").action(() => scopes.outputHelp());
5601
+ const scopeList = scopes.command("list").description("List every IAM scope in the tenant (unbound scopes have no resources)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
5602
+ "after",
5603
+ examples("airs aigateway scopes list", "airs aigateway scopes list --output json")
5604
+ ).action(async (opts) => {
5605
+ try {
5606
+ const fmt = await resolveOutput(scopeList, opts);
5607
+ if (fmt === "pretty") renderAiGatewayHeader();
5608
+ const service = await createService();
5609
+ renderScopeList(await service.listScopes(), fmt);
5610
+ } catch (err) {
5611
+ failWithGrantHint(err);
5612
+ }
5613
+ });
5614
+ const scopeGet = scopes.command("get <name>").description("Get one IAM scope by name").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText("after", examples("airs aigateway scopes get ws_production_bx7qw0")).action(async (name, opts) => {
5615
+ try {
5616
+ const fmt = await resolveOutput(scopeGet, opts);
5617
+ if (fmt === "pretty") renderAiGatewayHeader();
5618
+ const service = await createService();
5619
+ renderScopeDetail(await service.getScope(name), fmt);
5620
+ } catch (err) {
5621
+ failWithGrantHint(err);
5622
+ }
5623
+ });
5624
+ const scopeCreate = scopes.command("create").description("Create an unbound IAM scope (step 1 of provisioning, on its own)").requiredOption("--name <name>", "Scope name, e.g. ws_production_bx7qw0").option("--description <text>", "Scope description").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5625
+ "after",
5626
+ examples(
5627
+ `airs aigateway scopes create --name ws_production_bx7qw0 --description 'All production applications'`
5628
+ )
5629
+ ).action(async (opts) => {
5630
+ try {
5631
+ const fmt = await resolveOutput(scopeCreate, opts, { allowed: ["pretty", "json", "yaml"] });
5632
+ if (fmt === "pretty") renderAiGatewayHeader();
5633
+ const service = await createService();
5634
+ const scope = await service.createScope({
5635
+ name: opts.name,
5636
+ ...opts.description !== void 0 ? { description: opts.description } : {}
5637
+ });
5638
+ ui.success(`IAM scope created: ${scope.name}`);
5639
+ ui.status(
5640
+ "The scope is not bound to anything yet \u2014 `workspaces create --existing-scope` or `scopes bind` finishes the job."
5641
+ );
5642
+ renderScopeDetail(scope, fmt);
5643
+ } catch (err) {
5644
+ failWithGrantHint(err);
5645
+ }
5646
+ });
5647
+ const scopeBind = scopes.command("bind <name>").description("Bind a workspace to an existing IAM scope (step 3 of provisioning, on its own)").requiredOption("--workspace <ref>", "Workspace slug, UUID, or unique display name").option("--output <format>", "Output format: pretty, json, yaml", "pretty").addHelpText(
5648
+ "after",
5649
+ examples("airs aigateway scopes bind ws_production_bx7qw0 --workspace ws-produc-985697")
5650
+ ).action(async (name, opts) => {
5651
+ try {
5652
+ const fmt = await resolveOutput(scopeBind, opts, { allowed: ["pretty", "json", "yaml"] });
5653
+ if (fmt === "pretty") renderAiGatewayHeader();
5654
+ const service = await createService();
5655
+ const scope = await service.bindScope(name, opts.workspace);
5656
+ ui.success(`IAM scope ${scope.name} now binds ${scope.resources.length} resource(s)`);
5657
+ renderScopeDetail(scope, fmt);
5658
+ } catch (err) {
5659
+ failWithGrantHint(err);
5660
+ }
5661
+ });
5662
+ scopes.command("delete <name>").alias("rm").description("Delete an IAM scope by name (not live-verified upstream; the API may decline)").option("--force", "Skip confirmation prompt").addHelpText("after", examples("airs aigateway scopes delete ws_truffles_ggolfu --force")).action(async (name, opts) => {
5663
+ try {
5664
+ renderAiGatewayHeader();
5665
+ await confirmOrAbort(
5666
+ `Delete IAM scope ${name}? Workspaces bound to it lose their data-plane grant.`,
5667
+ Boolean(opts.force),
5668
+ { action: `delete IAM scope ${name}` }
5669
+ );
5670
+ const service = await createService();
5671
+ await service.deleteScope(name);
5672
+ ui.success(`IAM scope deleted: ${name}`);
5673
+ } catch (err) {
5674
+ failWithGrantHint(err);
5675
+ }
5676
+ });
5495
5677
  const telemetry = aigateway.command("telemetry").description("AI Gateway runtime telemetry (data plane)").action(() => telemetry.outputHelp());
5496
5678
  registerAiGatewayTelemetryReads(telemetry);
5497
5679
  const cost = addChartFilterOptions(
package/dist/index.d.ts CHANGED
@@ -1666,6 +1666,7 @@ declare const ConfigSchema: z.ZodObject<{
1666
1666
  aiGwDataEndpoint: z.ZodOptional<z.ZodString>;
1667
1667
  aiGwAdminEndpoint: z.ZodOptional<z.ZodString>;
1668
1668
  aiGwTokenEndpoint: z.ZodOptional<z.ZodString>;
1669
+ iamEndpoint: z.ZodOptional<z.ZodString>;
1669
1670
  aiGwInferenceEndpoint: z.ZodOptional<z.ZodString>;
1670
1671
  aiGwInferenceApiKey: z.ZodOptional<z.ZodString>;
1671
1672
  aiGwInferenceModel: z.ZodOptional<z.ZodString>;
@@ -1700,6 +1701,7 @@ declare const ConfigSchema: z.ZodObject<{
1700
1701
  aiGwDataEndpoint?: string | undefined;
1701
1702
  aiGwAdminEndpoint?: string | undefined;
1702
1703
  aiGwTokenEndpoint?: string | undefined;
1704
+ iamEndpoint?: string | undefined;
1703
1705
  aiGwInferenceEndpoint?: string | undefined;
1704
1706
  aiGwInferenceApiKey?: string | undefined;
1705
1707
  aiGwInferenceModel?: string | undefined;
@@ -1730,6 +1732,7 @@ declare const ConfigSchema: z.ZodObject<{
1730
1732
  aiGwDataEndpoint?: string | undefined;
1731
1733
  aiGwAdminEndpoint?: string | undefined;
1732
1734
  aiGwTokenEndpoint?: string | undefined;
1735
+ iamEndpoint?: string | undefined;
1733
1736
  aiGwInferenceEndpoint?: string | undefined;
1734
1737
  aiGwInferenceApiKey?: string | undefined;
1735
1738
  aiGwInferenceModel?: string | undefined;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  validateTopic,
29
29
  writeBackupFile,
30
30
  writeReportFile
31
- } from "./chunk-XQOICLSH.js";
31
+ } from "./chunk-37SN57XQ.js";
32
32
  export {
33
33
  AirsScanService,
34
34
  SDK_ASYNC_BATCH_SIZE,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cdot65/prisma-airs-cli",
3
3
  "packageManager": "pnpm@10.6.5",
4
- "version": "5.10.0",
4
+ "version": "5.11.0",
5
5
  "description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "license": "MIT",
61
61
  "dependencies": {
62
- "@cdot65/prisma-airs-sdk": "0.30.1",
62
+ "@cdot65/prisma-airs-sdk": "0.31.0",
63
63
  "@inquirer/prompts": "^8.3.0",
64
64
  "chalk": "^5.6.2",
65
65
  "commander": "^14.0.3",