Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
23 changes: 23 additions & 0 deletions .changeset/runtime-customer-apps-consumption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@cdot65/prisma-airs-cli': minor
---

Add `airs runtime customer-apps consumption [appName]` for per-app token consumption + violation breakdown, sourced from the SCM AI Security > Runtime > API Applications dashboard endpoints (via the new `mgmt.dashboard` SDK namespace).

```
# pretty (default): per-app sections with tokens, sessions, firing detectors
airs runtime customer-apps consumption chatbot

# all apps in tenant (omit appName)
airs runtime customer-apps consumption

# 60-day window instead of default 30
airs runtime customer-apps consumption chatbot --time-interval 60

# structured outputs (table / csv / json / yaml) — one row per detector per app
airs runtime customer-apps consumption --output csv > consumption.csv
```

The API enforces an enum for `--time-interval`: only `7`, `30`, and `60` are accepted (verified live 2026-05-28; the CLI validates client-side before calling). The dashboard endpoints require both `appId` and `appName`, so the CLI resolves the UUID from the `customer-apps list` endpoint internally - users only supply the human-readable app name.

Closes #222.
2 changes: 2 additions & 0 deletions docs/cli/examples/.missing-allowlist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
runtime api-keys delete
# Blocked by upstream — see https://github.com/cdot65/prisma-airs-cli/issues/115
runtime customer-apps get
# Awaiting curated sidecars from PR #236 author — new command in v2.12.0
runtime customer-apps consumption
# Blocked by upstream API 503 — see https://github.com/cdot65/prisma-airs-cli/issues/193
runtime customer-apps update
# Blocked by SDK — see https://github.com/cdot65/prisma-airs-sdk/issues/167
Expand Down
26 changes: 26 additions & 0 deletions docs/cli/runtime/customer-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,29 @@ airs runtime customer-apps delete [options] <appName>

!!! warning "Example needed"
No curated input/output example for this command yet.

---

### runtime customer-apps consumption

Show per-app token consumption + violation breakdown (SCM dashboard). Omit appName to scan all apps.

```text
airs runtime customer-apps consumption [options] [appName]
```

#### Arguments

- `appName` (optional) —

#### Options

| Flag | Required | Default | Description |
|------|:--------:|---------|-------------|
| `--time-interval <n>` | No | `30` | Window in days: 7, 30, or 60 |
| `--output <format>` | No | `pretty` | Output format: pretty, table, csv, json, yaml |

#### Examples

!!! warning "Example needed"
No curated input/output example for this command yet.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@cdot65/prisma-airs-sdk": "^0.10.0",
"@cdot65/prisma-airs-sdk": "^0.11.0",
"@inquirer/prompts": "^8.3.0",
"@langchain/anthropic": "^1.3.25",
"@langchain/aws": "^1.3.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions src/airs/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { ProfileTopic } from '../audit/types.js';
import type {
ApiKeyInfo,
ApiKeyListResult,
ConsumptionQueryOptions,
CustomerAppConsumption,
CustomerAppInfo,
CustomerAppListResult,
DeleteResponse,
Expand Down Expand Up @@ -366,6 +368,69 @@ export class SdkManagementService implements ManagementService {
return this.normalizeCustomerApp(response as unknown as Record<string, unknown>);
}

async getCustomerAppConsumption(
appName: string,
opts?: ConsumptionQueryOptions,
): Promise<CustomerAppConsumption> {
// The dashboard endpoints need BOTH appId and appName; the only way to resolve appId
// from a name is via the list endpoint, so do that first.
const list = (await this.client.customerApps.list({
offset: 0,
limit: 100,
})) as unknown as { customer_apps?: Array<Record<string, unknown>> };
const apps = list.customer_apps ?? [];
const target = apps.find((a) => (a.app_name as string) === appName);
const appId = target?.customer_appId as string | undefined;
if (!appId) {
throw new Error(
`Customer app not found: "${appName}". Run \`airs runtime customer-apps list\` to see available apps.`,
);
}

const timeInterval = opts?.timeInterval ?? 30;
const query = { appId, appName, timeInterval } as const;

const [overview, breakdown] = await Promise.all([
this.client.dashboard.application(query),
this.client.dashboard.applicationViolationBreakdown(query),
]);

const ts = overview.token_stats ?? {};
const ss = overview.session_stats ?? {};
const detectors = (breakdown.detection_type_violation_breakdown ?? []).map((entry) => {
const vb = entry.violation_breakdown ?? {};
return {
type: entry.detection_type ?? 'unknown',
critical: vb.critical ?? 0,
high: vb.high ?? 0,
medium: vb.medium ?? 0,
low: vb.low ?? 0,
total: vb.total ?? 0,
};
});

return {
appId,
appName,
cloud: overview.cloud ?? undefined,
source: overview.source ?? undefined,
monitoringSince: overview.created_at ?? undefined,
profiles: overview.profiles ?? [],
tokens: {
dailyAverage: ts.average_daily_tokens ?? undefined,
dailyAverageScale: ts.average_daily_tokens_scale ?? undefined,
monthlyTotal: ts.monthly_total_tokens ?? undefined,
monthlyTotalScale: ts.monthly_total_tokens_scale ?? undefined,
},
sessions: {
total: ss.total ?? 0,
violating: ss.violating ?? 0,
},
detectors,
totalViolating: breakdown.total_violating ?? 0,
};
}

// -------------------------------------------------------------------------
// Deployment Profiles (read-only)
// -------------------------------------------------------------------------
Expand Down
51 changes: 51 additions & 0 deletions src/airs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,52 @@ export interface CustomerAppListResult {
nextOffset?: number;
}

/**
* Per-app consumption + violation snapshot, normalized from the SDK's dashboard endpoints.
* Time window is fixed at construction.
*/
export interface CustomerAppConsumption {
appId: string;
appName: string;
cloud?: string;
source?: string;
/** ISO timestamp of first monitoring (corresponds to SCM panel's "Monitoring Since"). */
monitoringSince?: string;
/** Attached security profile names. */
profiles: string[];
/** Token consumption stats with scale qualifier (K = thousands, M = millions). */
tokens: {
dailyAverage?: number;
dailyAverageScale?: string;
monthlyTotal?: number;
monthlyTotalScale?: string;
};
/** Session activity counts over the window. */
sessions: {
total: number;
violating: number;
};
/** Per-detector violation severity counts, one entry per detection_type. */
detectors: Array<{
type: string;
critical: number;
high: number;
medium: number;
low: number;
total: number;
}>;
/** Sum of violating sessions across all detectors (mirrors SCM panel's badge). */
totalViolating: number;
}

/** Allowed values for `--time-interval`. The API enforces this enum (other values return 400). */
export type ConsumptionTimeInterval = 7 | 30 | 60;

/** Options for {@link ManagementService.getCustomerAppConsumption}. */
export interface ConsumptionQueryOptions {
timeInterval?: ConsumptionTimeInterval;
}

// ---------------------------------------------------------------------------
// Deployment profile types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -931,6 +977,11 @@ export interface ManagementService {
getCustomerApp(appName: string): Promise<CustomerAppInfo>;
updateCustomerApp(appId: string, request: Record<string, unknown>): Promise<CustomerAppInfo>;
deleteCustomerApp(appName: string, updatedBy: string): Promise<CustomerAppInfo>;
/** Get per-app token consumption + violation breakdown from the SCM dashboard endpoints. */
getCustomerAppConsumption(
appName: string,
opts?: ConsumptionQueryOptions,
): Promise<CustomerAppConsumption>;

// Deployment profiles
listDeploymentProfiles(opts?: { unactivated?: boolean }): Promise<DeploymentProfileInfo[]>;
Expand Down
51 changes: 51 additions & 0 deletions src/cli/commands/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type OutputFormat,
renderApiKeyDetail,
renderApiKeyList,
renderCustomerAppConsumption,
renderCustomerAppDetail,
renderCustomerAppList,
renderDeploymentProfileList,
Expand Down Expand Up @@ -314,6 +315,56 @@ export function registerRuntimeCommand(program: Command): void {
}
});

customerApps
.command('consumption [appName]')
.description(
'Show per-app token consumption + violation breakdown (SCM dashboard). Omit appName to scan all apps.',
)
.option('--time-interval <n>', 'Window in days: 7, 30, or 60', '30')
.option('--output <format>', 'Output format: pretty, table, csv, json, yaml', 'pretty')
.action(async (appName: string | undefined, opts) => {
try {
const fmt = opts.output as OutputFormat;
const interval = Number.parseInt(opts.timeInterval, 10);
if (interval !== 7 && interval !== 30 && interval !== 60) {
renderError('--time-interval must be 7, 30, or 60 (the API rejects other values)');
process.exit(1);
}
if (fmt === 'pretty') renderRuntimeConfigHeader();

const service = await createMgmtService();

// Single app mode: explicit name was given.
if (appName) {
const data = await service.getCustomerAppConsumption(appName, {
timeInterval: interval,
});
renderCustomerAppConsumption(data, fmt);
return;
}

// All-apps mode: loop the list and emit one record per app.
const list = await service.listCustomerApps({ limit: 100 });
if (list.apps.length === 0) {
console.log(' No customer apps found.');
return;
}
for (const app of list.apps) {
try {
const data = await service.getCustomerAppConsumption(app.name, {
timeInterval: interval,
});
renderCustomerAppConsumption(data, fmt);
} catch (err) {
renderError(`[${app.name}] ${err instanceof Error ? err.message : String(err)}`);
}
}
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});

// -----------------------------------------------------------------------
// runtime deployment-profiles — read-only listing
// -----------------------------------------------------------------------
Expand Down
Loading
Loading