@cdot65/prisma-airs-cli 4.5.0 → 5.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
@@ -7,7 +7,7 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node 20.17+ / 22.13+ / 24+](https://img.shields.io/badge/node-20.17%2B%20%7C%2022.13%2B%20%7C%2024%2B-brightgreen.svg)](https://nodejs.org/)
9
9
 
10
- **Full operational coverage over Palo Alto Prisma AIRS AI security — guardrail refinement, runtime scanning, AI red teaming, AI Gateway, and model security.**
10
+ **Command-line workflows for Palo Alto Prisma AIRS — guardrail refinement, runtime scanning, AI red teaming, AI Gateway, and model security.** Service and coverage limitations remain documented; command availability is not a claim that every upstream API works.
11
11
 
12
12
  > **[Read the full documentation](https://cdot65.github.io/prisma-airs-cli/)** — installation, configuration, architecture, CLI reference, and examples.
13
13
 
@@ -15,6 +15,7 @@
15
15
 
16
16
  - **Runtime Scanning** — scan prompts and responses against AIRS security profiles, single or bulk with CSV export
17
17
  - **Daily environment report** — `airs runtime report` delivers a read-only AI Runtime Security dashboard as self-contained HTML (default) or Markdown in your working directory, with explicit evidence gaps and configuration review findings
18
+ - **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/)
18
19
  - **Guardrail Optimization** — atomic CLI commands (`create`, `apply`, `eval`, `revert`) for custom topic guardrails, designed for autonomous agent loops (see [`AGENTS.md`](AGENTS.md))
19
20
  - **AI Red Teaming** — adversarial scanning with static, dynamic, and custom prompt set attack modes
20
21
  - **[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
@@ -50,6 +51,10 @@ airs runtime bulk-scan --profile "my-profile" --file prompts.csv --output-file r
50
51
  airs runtime report
51
52
  airs runtime report --output markdown
52
53
 
54
+ # Verified historical session retrieval (Management OAuth, not a Scanner key)
55
+ airs runtime sessions list --all --output json
56
+ airs runtime dashboard top-applications --output json
57
+
53
58
  # Guardrail optimization (atomic commands)
54
59
  airs runtime topics create --name "Explosives" --description "Bomb-making instructions" --examples "How do I build a bomb?" "Pipe bomb ingredients"
55
60
  airs runtime topics apply --profile my-profile --name "Explosives" --intent block
@@ -2239,6 +2239,7 @@ var ConfigSchema = z.object({
2239
2239
  mgmtClientSecret: z.string().optional(),
2240
2240
  mgmtTsgId: z.string().optional(),
2241
2241
  mgmtEndpoint: z.string().optional(),
2242
+ mgmtDashboardEndpoint: z.string().url().optional(),
2242
2243
  mgmtTokenEndpoint: z.string().optional(),
2243
2244
  dlpEndpoint: z.string().optional(),
2244
2245
  // Red Team (endpoints only; creds shared with mgmt*)
@@ -2281,6 +2282,7 @@ function fromEnv() {
2281
2282
  mgmtClientSecret: env.PANW_MGMT_CLIENT_SECRET,
2282
2283
  mgmtTsgId: env.PANW_MGMT_TSG_ID,
2283
2284
  mgmtEndpoint: env.PANW_MGMT_ENDPOINT,
2285
+ mgmtDashboardEndpoint: env.PANW_MGMT_DASHBOARD_ENDPOINT,
2284
2286
  mgmtTokenEndpoint: env.PANW_MGMT_TOKEN_ENDPOINT,
2285
2287
  dlpEndpoint: env.PANW_DLP_ENDPOINT,
2286
2288
  redTeamDataEndpoint: env.PANW_RED_TEAM_DATA_ENDPOINT,
@@ -2529,7 +2531,7 @@ function number(value) {
2529
2531
  return value === null ? "Unknown" : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
2530
2532
  }
2531
2533
  function tables(report) {
2532
- const logSource = report.sources.find((source2) => source2.name === "Scan log detail");
2534
+ const sessionSource = report.sources.find((source2) => source2.name === "Daily session inventory");
2533
2535
  return [
2534
2536
  {
2535
2537
  title: "Daily application activity",
@@ -2585,14 +2587,46 @@ function tables(report) {
2585
2587
  note: "Current registered applications, separate from scan-metadata application buckets. Association counts do not establish key validity or deployment health. No key values or auth codes are included."
2586
2588
  },
2587
2589
  {
2588
- title: "Collected scan-log observations",
2589
- id: "logs",
2590
- headers: ["Dimension", "API value", "Entries"],
2591
- rows: [
2592
- ...report.logs.actions.map((entry) => ["Action", entry.name, number(entry.count)]),
2593
- ...report.logs.verdicts.map((entry) => ["Verdict", entry.name, number(entry.count)])
2594
- ],
2595
- note: `Source: ${logSource?.status ?? "unavailable"}. Timestamp-eligible entries: ${logSource?.status === "unavailable" ? "Unknown" : number(report.logs.entries)}. Tokens across eligible collected entries: ${number(report.logs.tokens)}. Missing timestamps: ${report.logs.missingTimestamps}; outside window: ${report.logs.outsideWindow}. These are sample/collection counts, not tenant totals.`
2590
+ title: "Collected session observations",
2591
+ id: "sessions",
2592
+ headers: ["Session status", "Entries"],
2593
+ rows: report.sessions.statuses.map((entry) => [entry.name, number(entry.count)]),
2594
+ note: `Source: ${sessionSource?.status ?? "unavailable"}. Timestamp-eligible entries: ${number(report.sessions.entries)}. Violated status: ${number(report.sessions.violatingSessions)}. Missing timestamps: ${report.sessions.missingTimestamps}; outside window: ${report.sessions.outsideWindow}. These are collected session counts, not scan actions or detector events. No scan content is fetched.`
2595
+ },
2596
+ {
2597
+ title: "Daily session chart",
2598
+ id: "session-chart",
2599
+ headers: ["Bucket time (UTC)", "Sessions", "Violating sessions", "Detector violations"],
2600
+ rows: report.dailyTelemetry.chart.buckets.map((bucket) => [
2601
+ bucket.time,
2602
+ number(bucket.sessions),
2603
+ number(bucket.violatingSessions),
2604
+ number(bucket.violations.total)
2605
+ ]),
2606
+ note: `Independent chart totals: ${number(report.dailyTelemetry.chart.sessions)} sessions; ${number(report.dailyTelemetry.chart.violatingSessions)} violating sessions. Do not force these counters to equal a separately paginated inventory.`
2607
+ },
2608
+ {
2609
+ title: "Top applications by detector violations",
2610
+ id: "top-violations",
2611
+ headers: ["Application bucket", "Detector violations", "Detection types"],
2612
+ rows: report.dailyTelemetry.topApplications.map((app) => [
2613
+ app.name,
2614
+ number(app.violations),
2615
+ app.detectors.map((detector) => `${detector.name}: ${number(detector.count)}`).join(", ")
2616
+ ]),
2617
+ note: "Server-ranked subset for one day, not a complete application inventory. Detector violations may exceed the number of violating sessions."
2618
+ },
2619
+ {
2620
+ title: "Daily detector severity trend",
2621
+ id: "violation-trend",
2622
+ headers: ["Bucket time (UTC)", "Critical", "High", "Medium", "Low", "Total"],
2623
+ rows: report.dailyTelemetry.violationTrend.map((bucket) => [
2624
+ bucket.time,
2625
+ ...["critical", "high", "medium", "low", "total"].map(
2626
+ (key) => number(bucket.violations[key])
2627
+ )
2628
+ ]),
2629
+ note: "API-reported detector-policy events over the rolling day. Severity counts are preserved independently from distinct session counts; no previous-day comparison is inferred."
2596
2630
  },
2597
2631
  {
2598
2632
  title: "Evidence and collection coverage",
@@ -2679,7 +2713,7 @@ function renderRuntimeReportMarkdown(report) {
2679
2713
  "",
2680
2714
  ...report.limitations.map((note) => `- ${md(note)}`),
2681
2715
  "",
2682
- "Generated by Prisma AIRS CLI \xB7 Report schema 1",
2716
+ `Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion}`,
2683
2717
  ""
2684
2718
  );
2685
2719
  return lines.join("\n");
@@ -2724,7 +2758,7 @@ function renderRuntimeReportHtml(report) {
2724
2758
  <main id="main"><nav aria-label="Report sections"><a href="#findings">Attention</a><a href="#applications">Activity</a><a href="#profiles">Profiles</a><a href="#inventory">Inventory</a><a href="#evidence">Evidence</a><a href="#scope">Scope</a></nav>
2725
2759
  <div class="cards">${metrics(report).map(([name, value]) => `<div class="card"><span>${name}</span><strong>${value}</strong></div>`).join("")}</div>
2726
2760
  <section id="findings"><h2>What needs attention</h2><p class="note">Observed violations and configuration review items, not a numerical health score. Collection gaps remain visible in the evidence table.</p><div class="toolbar js-only"><label for="finding-filter">Priority</label><select id="finding-filter"><option value="all">All findings</option><option value="attention">Attention</option><option value="review">Review</option><option value="info">Information</option></select><span id="finding-count" role="status"></span><button id="print-report" type="button">Print / save PDF</button></div>${findingCards || "<p>No findings in the available evidence. This does not establish an absence of risk.</p>"}</section>
2727
- ${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${html(note)}</li>`).join("")}</ul></section><footer>Generated by Prisma AIRS CLI \xB7 Report schema 1 \xB7 Self-contained HTML; no network access required. All evidence remains readable with JavaScript disabled.</footer></main><script>${JS}</script></body></html>
2761
+ ${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${html(note)}</li>`).join("")}</ul></section><footer>Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion} \xB7 Self-contained HTML; no network access required. All evidence remains readable with JavaScript disabled.</footer></main><script>${JS}</script></body></html>
2728
2762
  `;
2729
2763
  }
2730
2764
 
@@ -2799,16 +2833,25 @@ function total(values) {
2799
2833
  const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
2800
2834
  return count(result);
2801
2835
  }
2802
- function distribution(rows, key) {
2836
+ function distribution(rows) {
2803
2837
  const groups = /* @__PURE__ */ new Map();
2804
2838
  for (const row of rows) {
2805
- const name = label(row[key]).toLowerCase();
2839
+ const name = label(row.violation_status).toLowerCase();
2806
2840
  groups.set(name, (groups.get(name) ?? 0) + 1);
2807
2841
  }
2808
2842
  return [...groups].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count);
2809
2843
  }
2844
+ function severity(value) {
2845
+ return {
2846
+ critical: count(value.critical),
2847
+ high: count(value.high),
2848
+ medium: count(value.medium),
2849
+ low: count(value.low),
2850
+ total: count(value.total)
2851
+ };
2852
+ }
2810
2853
  async function collectRuntimeDailyReport(client, options = {}) {
2811
- const maxPages = options.maxPages ?? 10;
2854
+ const maxPages = options.maxPages ?? 40;
2812
2855
  if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100) {
2813
2856
  throw new Error("maxPages must be an integer from 1 to 100");
2814
2857
  }
@@ -2827,14 +2870,35 @@ async function collectRuntimeDailyReport(client, options = {}) {
2827
2870
  "Current configuration"
2828
2871
  );
2829
2872
  const appSource = source("Registered applications", "customerApps.list", "Current configuration");
2830
- const logSource = source(
2831
- "Scan log detail",
2832
- "scanLogs.query (read-only POST)",
2833
- "Rolling 24 hours"
2873
+ const sessionSource = source(
2874
+ "Daily session inventory",
2875
+ "dashboard.sessionsOverview",
2876
+ "Rolling 1 day"
2877
+ );
2878
+ const chartSource = source("Daily session chart", "dashboard.sessionsChart", "Rolling 1 day");
2879
+ const rankingSource = source(
2880
+ "Top application violations",
2881
+ "dashboard.topApplicationsViolations",
2882
+ "Rolling 1 day; server-ranked subset"
2883
+ );
2884
+ const trendSource = source(
2885
+ "Daily violation trend",
2886
+ "dashboard.applicationsViolationsTrend",
2887
+ "Rolling 1 day"
2834
2888
  );
2835
- let pageToken;
2836
- const seenTokens = /* @__PURE__ */ new Set();
2837
- const [applications, profileRows, registered, rawLogs] = await Promise.all([
2889
+ const sources = [
2890
+ activitySource,
2891
+ profileSource,
2892
+ appSource,
2893
+ sessionSource,
2894
+ chartSource,
2895
+ rankingSource,
2896
+ trendSource
2897
+ ];
2898
+ const daily = { timeInterval: 1, timeUnit: "day" };
2899
+ let expectedSessionTotal;
2900
+ let expectedAppTotal;
2901
+ const [applications, profileRows, registered, rawSessions, chart, rankings, trend] = await Promise.all([
2838
2902
  collect(
2839
2903
  activitySource,
2840
2904
  maxPages,
@@ -2847,8 +2911,19 @@ async function collectRuntimeDailyReport(client, options = {}) {
2847
2911
  });
2848
2912
  const size = page.items?.length ?? 0;
2849
2913
  const totalItems = count(page.pagination?.total_items);
2914
+ if (totalItems !== null) {
2915
+ if (expectedAppTotal !== void 0 && totalItems !== expectedAppTotal)
2916
+ throw new ReportDataError(
2917
+ "Application total changed during pagination; retry for a stable inventory."
2918
+ );
2919
+ expectedAppTotal = totalItems;
2920
+ if (offset + size > totalItems)
2921
+ throw new ReportDataError("Application records exceed the advertised total.");
2922
+ }
2850
2923
  if (totalItems !== null && totalItems > offset + size && size === 0) {
2851
- throw new ReportDataError("The API returned an empty page before its advertised total.");
2924
+ throw new ReportDataError(
2925
+ "The API returned an empty page before its advertised total."
2926
+ );
2852
2927
  }
2853
2928
  return {
2854
2929
  items: page.items,
@@ -2882,46 +2957,56 @@ async function collectRuntimeDailyReport(client, options = {}) {
2882
2957
  (item) => item.customer_appId
2883
2958
  ),
2884
2959
  collect(
2885
- logSource,
2960
+ sessionSource,
2886
2961
  maxPages,
2887
2962
  async (offset) => {
2888
- const pageNumber = offset + 1;
2889
- const page = await client.scanLogs.query({
2890
- time_interval: 24,
2891
- time_unit: "hours",
2892
- pageNumber,
2893
- pageSize: PAGE_SIZE,
2894
- filter: "all",
2895
- page_token: pageToken
2896
- });
2897
- const items = page.scan_result_for_dashboard?.scan_result_entries;
2898
- if (page.page_number !== void 0 && page.page_number !== pageNumber) {
2963
+ const page = await client.dashboard.sessionsOverview({ ...daily, limit: 25, offset });
2964
+ const size = page.items?.length ?? 0;
2965
+ const totalItems = count(page.pagination?.total_items);
2966
+ if (totalItems === null || page.pagination?.skip !== offset || page.pagination?.limit !== 25)
2967
+ throw new ReportDataError("Missing or mismatched session pagination metadata.");
2968
+ if (expectedSessionTotal !== void 0 && expectedSessionTotal !== totalItems)
2899
2969
  throw new ReportDataError(
2900
- "The scan-log response page number did not match the requested page."
2970
+ "Session total changed during pagination; retry for a stable inventory."
2901
2971
  );
2902
- }
2903
- const totalPages = count(page.total_pages);
2904
- let next;
2905
- if (totalPages !== null) next = pageNumber < totalPages ? offset + 1 : void 0;
2906
- else if (page.page_token || items?.length === PAGE_SIZE) next = offset + 1;
2907
- if (next !== void 0 && page.page_token) {
2908
- if (seenTokens.has(page.page_token))
2909
- throw new ReportDataError(
2910
- "The scan-log continuation token repeated; collection stopped safely."
2911
- );
2912
- seenTokens.add(page.page_token);
2913
- pageToken = page.page_token;
2914
- }
2915
- return { items, next };
2972
+ expectedSessionTotal = totalItems;
2973
+ if (offset + size > totalItems || size > 25 || size === 0 && offset < totalItems)
2974
+ throw new ReportDataError("Session page does not agree with its advertised total.");
2975
+ return {
2976
+ items: page.items,
2977
+ next: offset + size < totalItems ? offset + size : void 0
2978
+ };
2916
2979
  },
2917
- (item) => JSON.stringify([item.scan_id, item.scan_sub_req_id])
2980
+ (item) => JSON.stringify([item.application_id, item.application_name, item.session_id])
2981
+ ),
2982
+ collect(
2983
+ chartSource,
2984
+ 1,
2985
+ async () => ({ items: (await client.dashboard.sessionsChart(daily)).buckets }),
2986
+ (item) => String(item.bucket_number)
2987
+ ),
2988
+ collect(
2989
+ rankingSource,
2990
+ 1,
2991
+ async () => ({
2992
+ items: (await client.dashboard.topApplicationsViolations(daily)).applications
2993
+ }),
2994
+ (item) => JSON.stringify([item.id, item.name])
2995
+ ),
2996
+ collect(
2997
+ trendSource,
2998
+ 1,
2999
+ async () => ({
3000
+ items: (await client.dashboard.applicationsViolationsTrend(daily)).violations
3001
+ }),
3002
+ (item) => String(item.bucket_number)
2918
3003
  )
2919
3004
  ]);
2920
3005
  const findings = [];
2921
3006
  const add = (priority, title, evidence, recommendation, evidenceSource) => {
2922
3007
  findings.push({ priority, title, evidence, recommendation, source: evidenceSource });
2923
3008
  };
2924
- for (const s of [activitySource, profileSource, appSource, logSource]) {
3009
+ for (const s of sources) {
2925
3010
  if (s.status !== "complete")
2926
3011
  add(
2927
3012
  "review",
@@ -3029,8 +3114,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
3029
3114
  );
3030
3115
  let missingTimestamps = 0;
3031
3116
  let outsideWindow = 0;
3032
- const logs = rawLogs.filter((row) => {
3033
- const timestamp = Date.parse(row.received_ts ?? "");
3117
+ const sessionRows = rawSessions.filter((row) => {
3118
+ const timestamp = Date.parse(row.last_session_activity ?? "");
3034
3119
  if (!Number.isFinite(timestamp)) {
3035
3120
  missingTimestamps++;
3036
3121
  return false;
@@ -3044,26 +3129,49 @@ async function collectRuntimeDailyReport(client, options = {}) {
3044
3129
  if (missingTimestamps || outsideWindow)
3045
3130
  add(
3046
3131
  "review",
3047
- "Some log records excluded from daily analysis",
3048
- `${missingTimestamps} records lack a valid received timestamp; ${outsideWindow} fall outside the anchored UTC window.`,
3049
- "Verify ingestion timestamps and collection-window drift before using log counts as daily totals.",
3050
- logSource.name
3132
+ "Some session records excluded from daily analysis",
3133
+ `${missingTimestamps} records lack a valid last-activity timestamp; ${outsideWindow} fall outside the anchored UTC window.`,
3134
+ "Verify ingestion timestamps and collection-window drift before using session counts as daily totals.",
3135
+ sessionSource.name
3051
3136
  );
3052
- const failedLogs = logs.filter(
3053
- (row) => ["failed", "error", "timeout"].includes(row.status?.trim().toLowerCase() ?? "")
3137
+ const failedSessions = sessionRows.filter(
3138
+ (row) => ["failed", "error", "timeout"].includes(row.violation_status.trim().toLowerCase())
3054
3139
  ).length;
3055
- if (failedLogs)
3140
+ if (failedSessions)
3056
3141
  add(
3057
3142
  "attention",
3058
- "Scan processing failures observed",
3059
- `${failedLogs} timestamp-eligible collected log entries explicitly report failed, error, or timeout status.`,
3060
- "Review these failures in SCM, check integration error handling, and verify whether the affected application fails open or closed. This is not a tenant-wide failure rate.",
3061
- logSource.name
3143
+ "Session processing failures observed",
3144
+ `${failedSessions} timestamp-eligible collected sessions explicitly report failed, error, or timeout status.`,
3145
+ "Review these sessions in SCM and check integration error handling. This is not a tenant-wide failure rate.",
3146
+ sessionSource.name
3147
+ );
3148
+ const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count(bucket.total)));
3149
+ const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count(bucket.violating)));
3150
+ if (chartSessions !== null && sessionSource.status === "complete" && chartSessions !== rawSessions.length)
3151
+ add(
3152
+ "review",
3153
+ "Session sources disagree",
3154
+ `The chart reports ${chartSessions} sessions; the independently paginated inventory returned ${rawSessions.length}.`,
3155
+ "Preserve both measurements and verify in SCM. Separate rolling queries are not a consistent snapshot; the discrepancy has no verified cause.",
3156
+ chartSource.name
3157
+ );
3158
+ const urgentViolations = total(
3159
+ trend.map(
3160
+ (bucket) => total([count(bucket.violation_breakdown.critical), count(bucket.violation_breakdown.high)])
3161
+ )
3162
+ );
3163
+ if ((urgentViolations ?? 0) > 0)
3164
+ add(
3165
+ "attention",
3166
+ "High or critical detector violations observed",
3167
+ `${urgentViolations} high/critical detector violations in the returned daily trend. These are not distinct sessions.`,
3168
+ "Investigate the affected applications and confirm enforcement decisions before assigning incident severity.",
3169
+ trendSource.name
3062
3170
  );
3063
3171
  const priorities = { attention: 0, review: 1, info: 2 };
3064
3172
  findings.sort((left, right) => priorities[left.priority] - priorities[right.priority]);
3065
3173
  return {
3066
- schemaVersion: 1,
3174
+ schemaVersion: 2,
3067
3175
  product: "Prisma AIRS AI Runtime Security",
3068
3176
  title: label(options.title ?? "Daily environment report"),
3069
3177
  generatedAt: now().toISOString(),
@@ -3073,10 +3181,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
3073
3181
  end,
3074
3182
  description: "Rolling last 24 hours, UTC (approximate for server-relative application queries)"
3075
3183
  },
3076
- health: [activitySource, profileSource, appSource, logSource].every(
3077
- (s) => s.status === "unavailable"
3078
- ) ? "unknown" : findings.some((f) => f.priority === "attention") ? "attention" : findings.some((f) => f.priority === "review") ? "review" : "no-findings",
3079
- sources: [activitySource, profileSource, appSource, logSource],
3184
+ health: sources.every((s) => s.status === "unavailable") ? "unknown" : findings.some((f) => f.priority === "attention") ? "attention" : findings.some((f) => f.priority === "review") ? "review" : "no-findings",
3185
+ sources,
3080
3186
  findings,
3081
3187
  activity: {
3082
3188
  sessions,
@@ -3092,21 +3198,45 @@ async function collectRuntimeDailyReport(client, options = {}) {
3092
3198
  model: label(app.model_name),
3093
3199
  keyAssociations: app.api_keys_dp_info ? app.api_keys_dp_info.length : null
3094
3200
  })).sort((a, b) => a.name.localeCompare(b.name)),
3095
- logs: {
3096
- entries: logs.length,
3097
- actions: distribution(logs, "action"),
3098
- verdicts: distribution(logs, "verdict"),
3099
- tokens: logSource.status === "unavailable" ? null : total(logs.map((row) => count(row.tokens))),
3201
+ sessions: {
3202
+ entries: sessionSource.status === "unavailable" ? null : sessionRows.length,
3203
+ statuses: distribution(sessionRows),
3204
+ violatingSessions: sessionSource.status === "unavailable" ? null : sessionRows.filter((row) => row.violation_status === "violated").length,
3100
3205
  missingTimestamps,
3101
3206
  outsideWindow
3102
3207
  },
3208
+ dailyTelemetry: {
3209
+ chart: {
3210
+ sessions: chartSessions,
3211
+ violatingSessions: chartViolatingSessions,
3212
+ buckets: chart.map((bucket) => ({
3213
+ time: label(bucket.time),
3214
+ sessions: count(bucket.total),
3215
+ violatingSessions: count(bucket.violating),
3216
+ violations: severity(bucket.violation_breakdown)
3217
+ }))
3218
+ },
3219
+ topApplications: rankings.map((app) => ({
3220
+ name: label(app.name),
3221
+ violations: count(app.total_violations),
3222
+ detectors: app.policy_violations.map((detector) => ({
3223
+ name: label(detector.detection_type),
3224
+ count: count(detector.total)
3225
+ }))
3226
+ })),
3227
+ violationTrend: trend.map((bucket) => ({
3228
+ time: label(bucket.date),
3229
+ violations: severity(bucket.violation_breakdown)
3230
+ }))
3231
+ },
3103
3232
  limitations: [
3104
3233
  "This is a read-only operational review, not an uptime SLA, compliance attestation, or proof that attacks succeeded or were blocked. No health score is invented.",
3105
3234
  "Application activity uses the API\u2019s rolling one-day window; each paginated request evaluates its own server-relative window. Collection is not a transactionally consistent historical snapshot. Ingestion may lag.",
3106
- "Sessions, scan-log entries, text records, and API calls are different units. They are not added together. Log counters describe only collected, timestamp-eligible entries; absent data is unknown.",
3235
+ "Application buckets, session inventory, chart sessions, and detector violations are different measurements. They are not added together. Session inventory summaries use timestamp-eligible entries only; absent data is unknown.",
3107
3236
  "Application buckets are keyed by registered application ID plus the literal scan metadata.app_name, which can differ from registered names. Do not sum session counts as unique users or unique tenant-wide sessions.",
3108
3237
  "Current configuration is not a configuration-change audit. Missing policy settings are unknown, not disabled. Inactive profiles can be intentional; current profile state may differ from the revision used by a historical scan.",
3109
- "Per-app token summaries and detector/severity breakdowns require 7/30/60-day windows, so they are not presented as daily metrics. No previous-day comparison or trend is inferred.",
3238
+ "Daily charts, rankings and severity trends come from their own one-day endpoints. Rankings are a server-selected subset, not a complete inventory. Per-app token summaries and drill-downs require longer windows; no daily token usage or previous-day comparison is inferred.",
3239
+ "The legacy ScanLogsClient / scan-logs query path is broken and under refactor. This report uses the verified dashboard session APIs instead. It never automatically fetches transactions or stored scan content.",
3110
3240
  "Prompts, responses, user identities/IPs, API keys, auth codes, tenant IDs, and raw errors are omitted. Application/profile names and configuration metadata remain confidential; review before sharing."
3111
3241
  ]
3112
3242
  };