Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/0033-mgmt-dashboard-applications-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@cdot65/prisma-airs-sdk': minor
---

Add `mgmt.dashboard.applicationsOverview(opts?)` covering the dashboard's apps enumeration endpoint at `/v1/mgmt/dashboard/v2/apps/applicationsoverview`. This is the canonical apps-list source for dashboard reporting and is what the SCM UI's "AI Security > Runtime > API Applications" view uses to populate its list.

The dashboard buckets traffic by the literal `metadata.app_name` value scan payloads sent. A single registered customer-app (`customer_apps.customer_appId`) can therefore appear here as multiple items, one per distinct scan-payload name, when integrations override `app_name` in scan metadata (LiteLLM's `panw_prisma_airs` guardrail does this by default, for example). Enumerating from `applicationsOverview` rather than `customerApps.list` is necessary to see every dashboard bucket - the `id` field is the registered `customer_appId` UUID, and the `name` field is the scan-payload value. Pair this with `dashboard.application(...)` and `dashboard.applicationViolationBreakdown(...)` to drill into specific buckets.

API behavior verified live on 2026-05-29 and reflected in the type signature:

- `timeInterval` is narrowed to `1 | 7 | 30 | 60`; other values return HTTP 400.
- `timeUnit` accepts `'days'`, `'day'`, and `'hour'` (note the singular forms - this endpoint accepts wider values than the per-app `application` endpoint, which only accepts `'days'`).
- Valid combinations live-tested: `(7, days)`, `(30, days)`, `(60, days)`, `(1, day)`, `(1, hour)`. The SCM UI uses `(1, day)` by default; the SDK defaults to `(30, days)` to match the existing dashboard methods.
- `limit` and `offset` provide offset-based pagination; the SCM UI uses `limit=25`.

Response shape uses `z.passthrough()` and `nullable().optional()` for forward compatibility with future API fields. New exports: `DashboardApplicationsOverviewQuery`, `DashboardApplicationsOverview`, `DashboardApplicationsOverviewItem`, `DashboardApplicationSessionsBucket`, `DashboardPagination`, and their schemas.

Also clarifies the `DashboardAppQuery.appName` JSDoc to reference `applicationsOverview` as the canonical enumeration source, so callers know where to obtain valid `(appId, appName)` pairs when integration-supplied scan-payload names diverge from SCM-registered customer-app names.
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const MGMT_OAUTH_TOKEN_PATH = '/v1/mgmt/oauth/client_credential/accesstok
export const MGMT_DASHBOARD_APPLICATION_PATH = '/v1/mgmt/dashboard/v2/apps/application';
export const MGMT_DASHBOARD_APPLICATION_VIOLATION_BREAKDOWN_PATH =
'/v1/mgmt/dashboard/v2/apps/applicationviolationbreakdown';
export const MGMT_DASHBOARD_APPLICATIONS_OVERVIEW_PATH =
'/v1/mgmt/dashboard/v2/apps/applicationsoverview';

// DLP (Data Loss Prevention) API defaults
export const DEFAULT_DLP_ENDPOINT = 'https://api.dlp.paloaltonetworks.com';
Expand Down
101 changes: 96 additions & 5 deletions src/management/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {
MGMT_DASHBOARD_APPLICATION_PATH,
MGMT_DASHBOARD_APPLICATIONS_OVERVIEW_PATH,
MGMT_DASHBOARD_APPLICATION_VIOLATION_BREAKDOWN_PATH,
} from '../constants.js';
import { request } from '../http/request.js';
import type { AuthAdapter } from '../http/types.js';
import {
DashboardApplicationSchema,
DashboardApplicationsOverviewSchema,
DashboardApplicationViolationBreakdownSchema,
type DashboardApplication,
type DashboardApplicationsOverview,
type DashboardApplicationViolationBreakdown,
} from '../models/mgmt-dashboard.js';

Expand All @@ -32,7 +35,15 @@ export interface DashboardAppQuery {
* {@link import('./customer-apps.js').CustomerAppsClient.list}'s `customer_appId` field.
*/
appId: string;
/** Application display name. Required, non-empty. URL encoding is handled internally. */
/**
* Application display name as the dashboard tracks it - the literal `metadata.app_name`
* value scan payloads sent. This may differ from the `customer_apps.app_name` field when
* the integration overrides it (LiteLLM's `panw_prisma_airs` guardrail does this by default,
* for example). Required, non-empty. URL encoding is handled internally.
*
* To enumerate all valid `(appId, appName)` pairs the dashboard tracks, use
* {@link DashboardClient.applicationsOverview} - one item per dashboard bucket.
*/
appName: string;
/**
* Look-back window length, in days. Defaults to 30 (matches the SCM UI's "30 days" claim).
Expand All @@ -48,13 +59,43 @@ export interface DashboardAppQuery {
timeUnit?: 'days';
}

/**
* Query parameters for {@link DashboardClient.applicationsOverview}.
*
* Distinct from {@link DashboardAppQuery} - this endpoint takes no `appId`/`appName` (it is the
* enumeration source for those) and accepts a wider set of time windows.
*/
export interface DashboardApplicationsOverviewQuery {
/**
* Look-back window length. Accepted values vary by `timeUnit`:
* - `timeUnit: 'days'` accepts `7`, `30`, or `60`.
* - `timeUnit: 'day'` accepts `1`.
* - `timeUnit: 'hour'` accepts `1`.
*
* Defaults to `30`. Other combinations return HTTP 400.
*/
timeInterval?: 1 | 7 | 30 | 60;
/**
* Look-back window unit. The dashboard apps-overview endpoint accepts `'days'`, `'day'`,
* and `'hour'` (note the singular forms - unlike the per-app `application` endpoint, which
* only accepts `'days'`). Defaults to `'days'`.
*/
timeUnit?: 'days' | 'day' | 'hour';
/** Maximum items to return. Defaults to 25 (matches the SCM UI). */
limit?: number;
/** Number of items to skip (offset-based pagination). Defaults to 0. */
offset?: number;
}

/**
* Client for AIRS SCM dashboard endpoints that power the
* "AI Security > Runtime > API Applications" detail panel.
* "AI Security > Runtime > API Applications" panel.
*
* Together, {@link application} (overview + token consumption + session activity) and
* {@link applicationViolationBreakdown} (per-detector violations) reproduce the full panel and
* unlock per-app token chargeback reporting.
* The dashboard buckets traffic by the **literal `metadata.app_name` value scan payloads
* actually sent**. A single registered customer-app (`customer_apps.customer_appId`) can therefore
* map to multiple dashboard buckets, one per distinct scan-payload name. Use
* {@link applicationsOverview} to enumerate all dashboard buckets, then {@link application} and
* {@link applicationViolationBreakdown} to drill into any specific `(id, name)` pair.
*
* @example
* ```ts
Expand Down Expand Up @@ -175,4 +216,54 @@ export class DashboardClient {
numRetries: this.numRetries,
});
}

/**
* Enumerate all dashboard application buckets the tenant has data for.
*
* This is the canonical apps-list source for the dashboard. The customer-apps endpoint
* (`mgmt.customerApps.list`) lists registered customer applications, but each registered
* customer-app can have **multiple dashboard buckets** when its API key is used to send scan
* requests carrying different `metadata.app_name` values - one bucket per distinct
* scan-payload name. This endpoint returns every bucket, so it is what you want for
* per-app token-consumption reporting and dashboard inventory.
*
* The `id` field on each item is the registered `customer_appId` UUID; the `name` field is
* the scan-payload value. Pass that `(id, name)` pair to {@link application} or
* {@link applicationViolationBreakdown} to retrieve drill-down data for a specific bucket.
*
* @param query - Time window and pagination.
* @returns Paginated bucket list plus pagination metadata.
* @example
* ```ts
* import { ManagementClient } from '@cdot65/prisma-airs-sdk';
* const mgmt = new ManagementClient();
*
* const { items } = await mgmt.dashboard.applicationsOverview();
* for (const item of items ?? []) {
* const overview = await mgmt.dashboard.application({
* appId: item.id ?? '',
* appName: item.name ?? '',
* });
* console.log(item.name, overview.token_stats?.monthly_total_tokens);
* }
* ```
*/
async applicationsOverview(
query?: DashboardApplicationsOverviewQuery,
): Promise<DashboardApplicationsOverview> {
return request({
method: 'GET',
baseUrl: this.baseUrl,
path: MGMT_DASHBOARD_APPLICATIONS_OVERVIEW_PATH,
params: {
time_interval: String(query?.timeInterval ?? 30),
time_unit: query?.timeUnit ?? 'days',
limit: String(query?.limit ?? 25),
offset: String(query?.offset ?? 0),
},
responseSchema: DashboardApplicationsOverviewSchema,
auth: this.auth,
numRetries: this.numRetries,
});
}
}
1 change: 1 addition & 0 deletions src/management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {
DashboardClient,
type DashboardClientOptions,
type DashboardAppQuery,
type DashboardApplicationsOverviewQuery,
} from './dashboard.js';
export { DlpNamespace, type DlpNamespaceOptions } from './dlp/index.js';
export {
Expand Down
8 changes: 8 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ export {
type DetectorViolationBreakdownEntry,
DashboardApplicationViolationBreakdownSchema,
type DashboardApplicationViolationBreakdown,
DashboardApplicationSessionsBucketSchema,
type DashboardApplicationSessionsBucket,
DashboardApplicationsOverviewItemSchema,
type DashboardApplicationsOverviewItem,
DashboardPaginationSchema,
type DashboardPagination,
DashboardApplicationsOverviewSchema,
type DashboardApplicationsOverview,
} from './mgmt-dashboard.js';
export {
DeploymentProfileEntrySchema,
Expand Down
78 changes: 78 additions & 0 deletions src/models/mgmt-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,81 @@ export const DashboardApplicationViolationBreakdownSchema = z
export type DashboardApplicationViolationBreakdown = z.infer<
typeof DashboardApplicationViolationBreakdownSchema
>;

/**
* One time bucket of session activity inside an `applicationsoverview` item.
*
* The dashboard returns a series of buckets covering the requested window, useful for spark-line
* style rendering. The exact shape varies by `time_unit`/`time_interval` combination; fields
* use forward-compatible parsing.
*/
export const DashboardApplicationSessionsBucketSchema = z
.object({
bucket_number: z.number().optional(),
date: z.string().nullable().optional(),
total: z.number().optional(),
violated: z.number().optional(),
})
.passthrough();

/** One session-activity bucket in a dashboard applications-overview item. */
export type DashboardApplicationSessionsBucket = z.infer<
typeof DashboardApplicationSessionsBucketSchema
>;

/**
* One application entry in the `applicationsoverview` response.
*
* The dashboard buckets traffic by **the literal `metadata.app_name` value scan payloads
* actually sent**. A single registered customer-app can therefore appear here as multiple items,
* one per distinct scan-payload name. The `id` field is the registered `customer_appId` UUID
* (matches `customer_apps.customer_appId`); the `name` field is the scan-payload value (which
* may differ from `customer_apps.app_name` when the integration overrides it).
*/
export const DashboardApplicationsOverviewItemSchema = z
.object({
id: z.string().nullable().optional(),
name: z.string().nullable().optional(),
cloud: z.string().nullable().optional(),
source: z.string().nullable().optional(),
created_at: z.string().nullable().optional(),
sessions: z.array(DashboardApplicationSessionsBucketSchema).nullable().optional(),
sessions_total: z.number().nullable().optional(),
sessions_violated: z.number().nullable().optional(),
})
.passthrough();

/** One application in the dashboard applications-overview response. */
export type DashboardApplicationsOverviewItem = z.infer<
typeof DashboardApplicationsOverviewItemSchema
>;

/** Pagination metadata on the applications-overview response. */
export const DashboardPaginationSchema = z
.object({
limit: z.number().optional(),
skip: z.number().optional(),
total_items: z.number().optional(),
})
.passthrough();

/** Pagination metadata returned by the applications-overview endpoint. */
export type DashboardPagination = z.infer<typeof DashboardPaginationSchema>;

/**
* Response from `dashboard/v2/apps/applicationsoverview`.
*
* Each item in `items[]` is one dashboard bucket (one per distinct scan-payload
* `metadata.app_name` per registered customer-app). Use this to enumerate all buckets the
* dashboard tracks, then call {@link DashboardClient.application} with each
* `(item.id, item.name)` pair to retrieve `token_stats` for that bucket.
*/
export const DashboardApplicationsOverviewSchema = z
.object({
items: z.array(DashboardApplicationsOverviewItemSchema).optional(),
pagination: DashboardPaginationSchema.optional(),
})
.passthrough();

/** Dashboard applications-overview response. */
export type DashboardApplicationsOverview = z.infer<typeof DashboardApplicationsOverviewSchema>;
92 changes: 92 additions & 0 deletions test/management/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,96 @@ describe('DashboardClient', () => {
expect(url).toContain('appname=chatbot');
});
});

describe('applicationsOverview', () => {
const OVERVIEW_RESPONSE = {
items: [
{
id: '5e16929a-1234-4567-89ab-cdef01234567',
name: 'chatbot',
cloud: 'other',
source: 'api',
created_at: '2026-04-29T19:00:56.098009282Z',
sessions: [
{ bucket_number: 0, date: '2026-04-29T19:00:56Z', total: 0, violated: 0 },
{ bucket_number: 1, date: '2026-04-30T19:00:56Z', total: 13104, violated: 504 },
],
sessions_total: 13104,
sessions_violated: 504,
},
{
id: '5e16929a-1234-4567-89ab-cdef01234567',
name: 'Claude Code',
cloud: 'other',
source: 'api',
created_at: '2026-04-29T19:00:56.098009282Z',
sessions: [],
sessions_total: 234,
sessions_violated: 12,
},
],
pagination: { limit: 25, skip: 0, total_items: 2 },
};

it('GETs /v1/mgmt/dashboard/v2/apps/applicationsoverview with defaults', async () => {
mockFetch(OVERVIEW_RESPONSE);
const result = await client.applicationsOverview();

expect(result.items).toHaveLength(2);
expect(result.items?.[0].name).toBe('chatbot');
expect(result.items?.[1].name).toBe('Claude Code');
expect(result.pagination?.total_items).toBe(2);

const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toContain('/v1/mgmt/dashboard/v2/apps/applicationsoverview');
expect(url).toContain('time_interval=30');
expect(url).toContain('time_unit=days');
expect(url).toContain('limit=25');
expect(url).toContain('offset=0');
});

it('exposes the (id, name) pair the application endpoint needs', async () => {
mockFetch(OVERVIEW_RESPONSE);
const result = await client.applicationsOverview();

// Both items share the same id (same registered customer_app) but have distinct names
// (each is a separate dashboard bucket driven by scan-payload metadata.app_name).
const ids = (result.items ?? []).map((i) => i.id);
const names = (result.items ?? []).map((i) => i.name);
expect(new Set(ids).size).toBe(1);
expect(new Set(names).size).toBe(2);
});

it('uses caller-supplied timeInterval / timeUnit / limit / offset', async () => {
mockFetch({ items: [], pagination: { limit: 100, skip: 50, total_items: 0 } });
await client.applicationsOverview({
timeInterval: 60,
timeUnit: 'days',
limit: 100,
offset: 50,
});

const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toContain('time_interval=60');
expect(url).toContain('time_unit=days');
expect(url).toContain('limit=100');
expect(url).toContain('offset=50');
});

it('supports the singular time_unit values the API accepts (day, hour)', async () => {
mockFetch({ items: [], pagination: { limit: 25, skip: 0, total_items: 0 } });
await client.applicationsOverview({ timeInterval: 1, timeUnit: 'hour' });

const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toContain('time_interval=1');
expect(url).toContain('time_unit=hour');
});

it('tolerates empty items[] without throwing', async () => {
mockFetch({ items: [], pagination: { limit: 25, skip: 0, total_items: 0 } });
const result = await client.applicationsOverview();
expect(result.items).toEqual([]);
expect(result.pagination?.total_items).toBe(0);
});
});
});
Loading