@cdot65/prisma-airs-cli 5.1.0 → 5.3.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 +5 -4
- package/dist/{chunk-DR6MZG4E.js → chunk-7Z76VCB3.js} +464 -126
- package/dist/cli/index.js +228 -82
- package/dist/index.d.ts +52 -9
- package/dist/index.js +7 -1
- package/package.json +2 -2
|
@@ -2483,6 +2483,433 @@ function computeCategoryBreakdown(results) {
|
|
|
2483
2483
|
})).sort((a, b) => b.errorRate - a.errorRate);
|
|
2484
2484
|
}
|
|
2485
2485
|
|
|
2486
|
+
// src/reports/aigateway.ts
|
|
2487
|
+
function record(v) {
|
|
2488
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("Invalid object");
|
|
2489
|
+
return v;
|
|
2490
|
+
}
|
|
2491
|
+
function numeric(v, integer = false) {
|
|
2492
|
+
if (v === null) return null;
|
|
2493
|
+
if (typeof v !== "number" || !Number.isFinite(v) || integer && (!Number.isSafeInteger(v) || v < 0))
|
|
2494
|
+
throw new Error("Invalid number");
|
|
2495
|
+
return v;
|
|
2496
|
+
}
|
|
2497
|
+
function count(v) {
|
|
2498
|
+
const n = numeric(v, true);
|
|
2499
|
+
if (n === null) throw new Error("Missing count");
|
|
2500
|
+
return n;
|
|
2501
|
+
}
|
|
2502
|
+
var CHARTS = {
|
|
2503
|
+
requests: ["total"],
|
|
2504
|
+
errors: ["total"],
|
|
2505
|
+
users: ["total"],
|
|
2506
|
+
cost: ["total", "avg"],
|
|
2507
|
+
latency: ["total", "p50", "p90", "p99"],
|
|
2508
|
+
tokens: ["total", "total_request_units", "total_response_units"],
|
|
2509
|
+
cacheSummary: [
|
|
2510
|
+
"summary.cacheHits",
|
|
2511
|
+
"summary.avgCacheLatency",
|
|
2512
|
+
"summary.totalRequests",
|
|
2513
|
+
"summary.cacheSpeedup"
|
|
2514
|
+
],
|
|
2515
|
+
cacheHitTrend: ["summary.totalCacheHits", "summary.hitRate"],
|
|
2516
|
+
userTrends: ["summary.total", "summary.unique", "summary.avg"],
|
|
2517
|
+
errorTrends: ["summary.errorPercent"],
|
|
2518
|
+
errorCategoryTrends: ["summary.totalErrors", "summary.totalUniqueErrorCodes"],
|
|
2519
|
+
groupedErrors: ["total"],
|
|
2520
|
+
rescuedRetries: ["total", "retryTotal", "fallbackTotal"],
|
|
2521
|
+
feedbackTrend: ["total"],
|
|
2522
|
+
feedbackWeighted: ["total"],
|
|
2523
|
+
feedbackScoreDistribution: ["total"],
|
|
2524
|
+
feedbackModels: []
|
|
2525
|
+
};
|
|
2526
|
+
async function collectGatewayEnvironmentReport(client, options) {
|
|
2527
|
+
const maxPages = options.maxPages ?? 40;
|
|
2528
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100)
|
|
2529
|
+
throw new Error("maxPages must be 1\u2013100");
|
|
2530
|
+
if (!options.workspace.trim() || !options.tsgId.trim() || !Number.isFinite(options.start.getTime()) || !Number.isFinite(options.end.getTime()) || options.start >= options.end)
|
|
2531
|
+
throw new Error("Invalid report scope");
|
|
2532
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2533
|
+
const report = {
|
|
2534
|
+
schemaVersion: 1,
|
|
2535
|
+
product: "Prisma AIRS AI Gateway",
|
|
2536
|
+
schemaLabel: "AI Gateway",
|
|
2537
|
+
windowLabel: "Telemetry window (UTC)",
|
|
2538
|
+
title: options.title ?? "AI Gateway daily report",
|
|
2539
|
+
collectionStartedAt: now().toISOString(),
|
|
2540
|
+
generatedAt: "",
|
|
2541
|
+
window: { start: options.start.toISOString(), end: options.end.toISOString() },
|
|
2542
|
+
health: "unknown",
|
|
2543
|
+
sources: [],
|
|
2544
|
+
findings: [],
|
|
2545
|
+
tables: [],
|
|
2546
|
+
limitations: [
|
|
2547
|
+
"Read-only operational evidence, not a security certification or availability SLA. No inference or configuration changes are performed.",
|
|
2548
|
+
"All telemetry uses the displayed fixed window. Configuration and organisation/catalog reads describe current state, not daily changes.",
|
|
2549
|
+
"Requests, errors, feedback and log totals are independent server aggregates, not an atomic snapshot. An AIRS block is not necessarily an outage.",
|
|
2550
|
+
"Cost values are cents; latency values are milliseconds. Feedback scores may be negative. Null means unknown, not zero.",
|
|
2551
|
+
"Only aggregate numbers and HTTP status codes are retained. Keys, user identities, workspace names/IDs, metadata, URLs, prompts, raw logs and configuration bodies are excluded.",
|
|
2552
|
+
"Catalog entries describe available capabilities, not enabled protection. Organisation limit sentinel values are not interpreted as quota exhaustion.",
|
|
2553
|
+
"Incomplete sources and page budgets are explicit. Current inventory lists have no verified continuation contract; total/has_more mismatches are partial."
|
|
2554
|
+
]
|
|
2555
|
+
};
|
|
2556
|
+
const metrics2 = {
|
|
2557
|
+
title: "Telemetry metrics",
|
|
2558
|
+
note: "Server field names are preserved. cost: cents; latency and avgCacheLatency: ms; errorPercent: percent. Feedback is not a request count.",
|
|
2559
|
+
headers: ["Source", "Metric", "Value"],
|
|
2560
|
+
rows: []
|
|
2561
|
+
};
|
|
2562
|
+
const trends = {
|
|
2563
|
+
title: "Error timeline",
|
|
2564
|
+
note: "Server UTC buckets; counts by HTTP status. Not a count of unique violating sessions.",
|
|
2565
|
+
headers: ["UTC bucket", "HTTP status", "Errors"],
|
|
2566
|
+
rows: []
|
|
2567
|
+
};
|
|
2568
|
+
const statuses = {
|
|
2569
|
+
title: "Error categories",
|
|
2570
|
+
note: "Server-reported errors by HTTP status; 446 denotes an AIRS block.",
|
|
2571
|
+
headers: ["HTTP status", "Errors"],
|
|
2572
|
+
rows: []
|
|
2573
|
+
};
|
|
2574
|
+
const inventory = {
|
|
2575
|
+
title: "Current configuration and capabilities",
|
|
2576
|
+
note: "Aggregate counts only. Active tenant workspaces are tenant-wide; keys and configs use the selected workspace.",
|
|
2577
|
+
headers: ["Source", "Metric", "Value"],
|
|
2578
|
+
rows: []
|
|
2579
|
+
};
|
|
2580
|
+
report.tables.push(metrics2, statuses, trends, inventory);
|
|
2581
|
+
async function source2(name, window2, collect2) {
|
|
2582
|
+
const s = {
|
|
2583
|
+
name,
|
|
2584
|
+
method: name,
|
|
2585
|
+
window: window2,
|
|
2586
|
+
status: "complete",
|
|
2587
|
+
records: 0,
|
|
2588
|
+
pages: 0,
|
|
2589
|
+
notes: []
|
|
2590
|
+
};
|
|
2591
|
+
report.sources.push(s);
|
|
2592
|
+
try {
|
|
2593
|
+
await collect2(s);
|
|
2594
|
+
} catch (error) {
|
|
2595
|
+
s.status = s.records ? "partial" : "unavailable";
|
|
2596
|
+
const code = error?.statusCode;
|
|
2597
|
+
s.notes.push(
|
|
2598
|
+
typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599 ? `HTTP ${code}: request failed; check access and service availability.` : "Request, scope or response validation failed; upstream details omitted."
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
function envelope(v, s) {
|
|
2603
|
+
const r = record(v);
|
|
2604
|
+
if (r.success !== true) throw new Error("Unsuccessful response");
|
|
2605
|
+
const d = record(r.data);
|
|
2606
|
+
if (d.isQuotaExceeded !== false) {
|
|
2607
|
+
s.status = "partial";
|
|
2608
|
+
s.notes.push("Quota exceeded or quota state unknown; aggregates may be incomplete.");
|
|
2609
|
+
}
|
|
2610
|
+
return d;
|
|
2611
|
+
}
|
|
2612
|
+
function list(v, s) {
|
|
2613
|
+
const r = record(v);
|
|
2614
|
+
if (r.success === false || !Array.isArray(r.data)) throw new Error("Invalid list");
|
|
2615
|
+
const rows = r.data.map(record);
|
|
2616
|
+
const total2 = count(r.total);
|
|
2617
|
+
if (r.has_more === true || total2 !== rows.length) {
|
|
2618
|
+
s.status = "partial";
|
|
2619
|
+
s.notes.push("List is truncated or total differs; continuation is not documented.");
|
|
2620
|
+
}
|
|
2621
|
+
s.records = rows.length;
|
|
2622
|
+
s.pages = 1;
|
|
2623
|
+
return rows;
|
|
2624
|
+
}
|
|
2625
|
+
let workspaceId;
|
|
2626
|
+
let workspaceSlug;
|
|
2627
|
+
await source2("workspaces.list", "Current active tenant workspaces (admin)", async (s) => {
|
|
2628
|
+
const rows = list(await client.workspaces.list({ plane: "admin" }), s);
|
|
2629
|
+
inventory.rows.push([s.name, "Active workspaces returned", rows.length]);
|
|
2630
|
+
const exact = rows.filter((r) => r.id === options.workspace || r.slug === options.workspace);
|
|
2631
|
+
const matches = exact.length ? exact : rows.filter((r) => r.name === options.workspace);
|
|
2632
|
+
if (matches.length !== 1 || typeof matches[0]?.id !== "string" || typeof matches[0]?.slug !== "string")
|
|
2633
|
+
throw new Error("Workspace missing or ambiguous");
|
|
2634
|
+
workspaceId = matches[0].id;
|
|
2635
|
+
workspaceSlug = matches[0].slug;
|
|
2636
|
+
});
|
|
2637
|
+
const window = () => {
|
|
2638
|
+
if (!workspaceSlug) throw new Error("Workspace unresolved");
|
|
2639
|
+
return { workspaceSlug, start: new Date(options.start), end: new Date(options.end) };
|
|
2640
|
+
};
|
|
2641
|
+
const scope = () => {
|
|
2642
|
+
if (!workspaceId) throw new Error("Workspace unresolved");
|
|
2643
|
+
return { workspaceId };
|
|
2644
|
+
};
|
|
2645
|
+
for (const [method, fields] of Object.entries(CHARTS)) {
|
|
2646
|
+
await source2(`telemetry.${method}`, "Displayed telemetry window", async (s) => {
|
|
2647
|
+
const d = envelope(await client.telemetry[method](window()), s);
|
|
2648
|
+
const rows = fields.map((field) => {
|
|
2649
|
+
const value = field.split(".").reduce((v, key) => record(v)[key], d);
|
|
2650
|
+
const n = numeric(value);
|
|
2651
|
+
const signed = method.startsWith("feedback");
|
|
2652
|
+
const fractional = signed || method === "cost" || method === "latency" || field.endsWith("avg") || field.endsWith("Latency") || field.endsWith("Speedup") || field.endsWith("Rate") || field.endsWith("Percent");
|
|
2653
|
+
if (n !== null && (!signed && n < 0 || !fractional && !Number.isSafeInteger(n)))
|
|
2654
|
+
throw new Error("Invalid metric");
|
|
2655
|
+
return [method, field, n];
|
|
2656
|
+
});
|
|
2657
|
+
const buckets = d.records ?? d.trend;
|
|
2658
|
+
if (buckets !== void 0 && !Array.isArray(buckets)) throw new Error("Invalid buckets");
|
|
2659
|
+
s.records = Array.isArray(buckets) ? buckets.length : 1;
|
|
2660
|
+
s.pages = 1;
|
|
2661
|
+
if (method === "feedbackModels") rows.push([method, "Models with feedback", s.records]);
|
|
2662
|
+
if (method === "errorCategoryTrends") {
|
|
2663
|
+
const entries = d.trend.map((v) => {
|
|
2664
|
+
const r = record(v);
|
|
2665
|
+
return [count(r.response_status_code), count(r.count)];
|
|
2666
|
+
});
|
|
2667
|
+
statuses.rows.push(...entries);
|
|
2668
|
+
if (entries.some(([code, n]) => code >= 200 && code < 300 && n > 0))
|
|
2669
|
+
report.findings.push({
|
|
2670
|
+
priority: "review",
|
|
2671
|
+
title: "Success-status responses appear in error analytics",
|
|
2672
|
+
evidence: "The server classifies some HTTP 2xx responses as errors. Error analytics are not identical to non-2xx transaction counts.",
|
|
2673
|
+
recommendation: "Inspect the underlying application/provider error classifications before calculating an HTTP failure rate.",
|
|
2674
|
+
source: s.name
|
|
2675
|
+
});
|
|
2676
|
+
if (entries.some(([code, n]) => n > 0 && (code === 429 || code >= 500)))
|
|
2677
|
+
report.findings.push({
|
|
2678
|
+
priority: "attention",
|
|
2679
|
+
title: "Rate limiting or server errors observed",
|
|
2680
|
+
evidence: "Error categories include HTTP 429 or 5xx responses in the selected window.",
|
|
2681
|
+
recommendation: "Review provider availability, quotas and retry policies; correlate with transaction logs.",
|
|
2682
|
+
source: s.name
|
|
2683
|
+
});
|
|
2684
|
+
else if (entries.some(([, n]) => n > 0))
|
|
2685
|
+
report.findings.push({
|
|
2686
|
+
priority: "review",
|
|
2687
|
+
title: "Gateway errors or policy blocks observed",
|
|
2688
|
+
evidence: "Nonzero error-category counts were returned.",
|
|
2689
|
+
recommendation: "Review status categories; distinguish expected AIRS blocks from integration failures.",
|
|
2690
|
+
source: s.name
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
if (method === "groupedErrors") {
|
|
2694
|
+
const entries = [];
|
|
2695
|
+
for (const v of d.trend) {
|
|
2696
|
+
const r = record(v);
|
|
2697
|
+
if (typeof r.x !== "string" || !Number.isFinite(Date.parse(r.x)) || !Array.isArray(r.y))
|
|
2698
|
+
throw new Error("Invalid trend");
|
|
2699
|
+
for (const v2 of r.y) {
|
|
2700
|
+
const y = record(v2);
|
|
2701
|
+
entries.push([
|
|
2702
|
+
new Date(r.x).toISOString(),
|
|
2703
|
+
count(y.response_status_code),
|
|
2704
|
+
count(y.count)
|
|
2705
|
+
]);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
trends.rows.push(...entries);
|
|
2709
|
+
}
|
|
2710
|
+
metrics2.rows.push(...rows);
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
await source2("telemetry.filterBoundaries", "Displayed telemetry window", async (s) => {
|
|
2714
|
+
const d = envelope(await client.telemetry.filterBoundaries(window()), s);
|
|
2715
|
+
for (const key of ["unique_ai_models", "unique_status_codes"]) {
|
|
2716
|
+
if (!Array.isArray(d[key])) throw new Error("Invalid filter values");
|
|
2717
|
+
metrics2.rows.push(["filterBoundaries", `${key} count`, d[key].length]);
|
|
2718
|
+
}
|
|
2719
|
+
s.records = 1;
|
|
2720
|
+
s.pages = 1;
|
|
2721
|
+
});
|
|
2722
|
+
const logs = {
|
|
2723
|
+
title: "Transaction coverage",
|
|
2724
|
+
note: "Unique collected transactions by HTTP status; only complete pagination represents the entire window. Raw rows are excluded.",
|
|
2725
|
+
headers: ["Metric", "Value"],
|
|
2726
|
+
rows: []
|
|
2727
|
+
};
|
|
2728
|
+
report.tables.push(logs);
|
|
2729
|
+
await source2("telemetry.logs", "Displayed telemetry window", async (s) => {
|
|
2730
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2731
|
+
const codes = /* @__PURE__ */ new Map();
|
|
2732
|
+
let expected;
|
|
2733
|
+
try {
|
|
2734
|
+
for (let currentPage = 0; currentPage < maxPages; currentPage++) {
|
|
2735
|
+
const d = envelope(
|
|
2736
|
+
await client.telemetry.logs({ ...window(), pageSize: 50, currentPage }),
|
|
2737
|
+
s
|
|
2738
|
+
);
|
|
2739
|
+
s.pages++;
|
|
2740
|
+
const total2 = count(d.total);
|
|
2741
|
+
expected ??= total2;
|
|
2742
|
+
if (total2 !== expected || !Array.isArray(d.records) || d.records.length > 50)
|
|
2743
|
+
throw new Error("Unstable page");
|
|
2744
|
+
for (const v of d.records) {
|
|
2745
|
+
const r = record(v);
|
|
2746
|
+
if (typeof r.id !== "string" || !r.id || ids.has(r.id) || r.workspace_slug !== workspaceSlug)
|
|
2747
|
+
throw new Error("Invalid identity");
|
|
2748
|
+
const timestamp = typeof r.created_at === "string" ? Date.parse(r.created_at) : NaN;
|
|
2749
|
+
if (!Number.isFinite(timestamp) || timestamp < options.start.getTime() || timestamp > options.end.getTime())
|
|
2750
|
+
throw new Error("Outside window");
|
|
2751
|
+
const code = count(r.response_status_code);
|
|
2752
|
+
ids.add(r.id);
|
|
2753
|
+
codes.set(code, (codes.get(code) ?? 0) + 1);
|
|
2754
|
+
s.records = ids.size;
|
|
2755
|
+
}
|
|
2756
|
+
if (ids.size > expected) throw new Error("Unexpected total");
|
|
2757
|
+
if (ids.size === expected) return;
|
|
2758
|
+
if (!d.records.length) throw new Error("Premature empty page");
|
|
2759
|
+
}
|
|
2760
|
+
s.status = "partial";
|
|
2761
|
+
s.notes.push(
|
|
2762
|
+
"Transaction page budget reached. Increase --max-pages to collect the remaining rows."
|
|
2763
|
+
);
|
|
2764
|
+
} finally {
|
|
2765
|
+
logs.rows.push(["Server total", expected ?? null], ["Unique collected", ids.size]);
|
|
2766
|
+
for (const [code, n] of [...codes].sort(([a], [b]) => a - b))
|
|
2767
|
+
logs.rows.push([`HTTP ${code}`, n]);
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
for (const method of ["configs.list", "apiKeys.listService", "apiKeys.listUser"]) {
|
|
2771
|
+
await source2(method, "Current selected workspace configuration", async (s) => {
|
|
2772
|
+
const response = method === "configs.list" ? await client.configs.list(scope()) : method === "apiKeys.listService" ? await client.apiKeys.listService(scope()) : await client.apiKeys.listUser(scope());
|
|
2773
|
+
const rows = list(response, s);
|
|
2774
|
+
inventory.rows.push([method, "Rows returned", rows.length]);
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
await source2("organisations.getInfo", "Current tenant configuration", async (s) => {
|
|
2778
|
+
const r = await client.organisations.getInfo(options.tsgId);
|
|
2779
|
+
inventory.rows.push([
|
|
2780
|
+
s.name,
|
|
2781
|
+
"Configured limit categories",
|
|
2782
|
+
Object.keys(r.benefits.limits).length
|
|
2783
|
+
]);
|
|
2784
|
+
s.records = 1;
|
|
2785
|
+
s.pages = 1;
|
|
2786
|
+
});
|
|
2787
|
+
await source2(
|
|
2788
|
+
"guardrails.getCatalog",
|
|
2789
|
+
"Current tenant capability catalog (not enabled guardrails)",
|
|
2790
|
+
async (s) => {
|
|
2791
|
+
const r = await client.guardrails.getCatalog();
|
|
2792
|
+
inventory.rows.push([s.name, "Available evaluators", r.evals.length]);
|
|
2793
|
+
s.records = r.evals.length;
|
|
2794
|
+
s.pages = 1;
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
for (const s of report.sources.filter((s2) => s2.status !== "complete"))
|
|
2798
|
+
report.findings.push({
|
|
2799
|
+
priority: "review",
|
|
2800
|
+
title: `Incomplete evidence: ${s.name}`,
|
|
2801
|
+
evidence: s.notes.join(" "),
|
|
2802
|
+
recommendation: "Resolve source access or validation problems and rerun. Do not interpret missing metrics as zero.",
|
|
2803
|
+
source: s.name
|
|
2804
|
+
});
|
|
2805
|
+
const telemetryAvailable = report.sources.some(
|
|
2806
|
+
(s) => s.name.startsWith("telemetry.") && s.status === "complete"
|
|
2807
|
+
);
|
|
2808
|
+
report.health = !telemetryAvailable ? "unknown" : report.findings.some((f) => f.priority === "attention") ? "attention" : report.findings.length ? "review" : "no-findings";
|
|
2809
|
+
report.generatedAt = now().toISOString();
|
|
2810
|
+
return report;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/reports/environment-render.ts
|
|
2814
|
+
import { createHash } from "crypto";
|
|
2815
|
+
var HEALTH = {
|
|
2816
|
+
attention: "Attention required",
|
|
2817
|
+
review: "Review required",
|
|
2818
|
+
"no-findings": "No findings in available evidence",
|
|
2819
|
+
unknown: "Unable to assess"
|
|
2820
|
+
};
|
|
2821
|
+
var html = (v) => String(v ?? "Unknown").replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
2822
|
+
var md = (v) => String(v ?? "Unknown").replace(
|
|
2823
|
+
/[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
|
|
2824
|
+
(c) => /[\r\n]/.test(c) ? " " : `&#${c.charCodeAt(0)};`
|
|
2825
|
+
);
|
|
2826
|
+
function tables(r) {
|
|
2827
|
+
return [
|
|
2828
|
+
...r.tables,
|
|
2829
|
+
{
|
|
2830
|
+
title: "Evidence and completeness",
|
|
2831
|
+
note: "Complete means the source was collected within its budget, not that the environment is secure. Unavailable is not zero.",
|
|
2832
|
+
headers: ["Source", "SDK method", "Window", "Status", "Records", "Pages", "Notes"],
|
|
2833
|
+
rows: r.sources.map((s) => [
|
|
2834
|
+
s.name,
|
|
2835
|
+
s.method,
|
|
2836
|
+
s.window,
|
|
2837
|
+
s.status,
|
|
2838
|
+
s.records,
|
|
2839
|
+
s.pages,
|
|
2840
|
+
s.notes.join(" ") || "Collection completed."
|
|
2841
|
+
])
|
|
2842
|
+
}
|
|
2843
|
+
];
|
|
2844
|
+
}
|
|
2845
|
+
function renderEnvironmentReportMarkdown(r) {
|
|
2846
|
+
const lines = [
|
|
2847
|
+
`# ${md(r.title)}`,
|
|
2848
|
+
"",
|
|
2849
|
+
`**${md(r.product)}**`,
|
|
2850
|
+
"",
|
|
2851
|
+
`Assessment: **${HEALTH[r.health]}**`,
|
|
2852
|
+
"",
|
|
2853
|
+
`Collection: ${r.collectionStartedAt} \u2192 ${r.generatedAt}`,
|
|
2854
|
+
"",
|
|
2855
|
+
`${md(r.windowLabel ?? "Scan creation window (UTC)")}: ${r.window.start} \u2192 ${r.window.end}`,
|
|
2856
|
+
"",
|
|
2857
|
+
"## What needs attention",
|
|
2858
|
+
""
|
|
2859
|
+
];
|
|
2860
|
+
for (const f of r.findings)
|
|
2861
|
+
lines.push(
|
|
2862
|
+
`### ${f.priority.toUpperCase()}: ${md(f.title)}`,
|
|
2863
|
+
"",
|
|
2864
|
+
`Evidence: ${md(f.evidence)}`,
|
|
2865
|
+
"",
|
|
2866
|
+
`Next step: ${md(f.recommendation)}`,
|
|
2867
|
+
"",
|
|
2868
|
+
`Source: ${md(f.source)}`,
|
|
2869
|
+
""
|
|
2870
|
+
);
|
|
2871
|
+
if (!r.findings.length)
|
|
2872
|
+
lines.push("No findings in available evidence; this does not establish absence of risk.", "");
|
|
2873
|
+
for (const t of tables(r)) {
|
|
2874
|
+
lines.push(`## ${md(t.title)}`, "", md(t.note), "");
|
|
2875
|
+
if (t.rows.length)
|
|
2876
|
+
lines.push(
|
|
2877
|
+
`| ${t.headers.map(md).join(" | ")} |`,
|
|
2878
|
+
`| ${t.headers.map(() => "---").join(" | ")} |`,
|
|
2879
|
+
...t.rows.map((row) => `| ${row.map(md).join(" | ")} |`),
|
|
2880
|
+
""
|
|
2881
|
+
);
|
|
2882
|
+
else
|
|
2883
|
+
lines.push(
|
|
2884
|
+
"No rows collected; consult source completeness before interpreting this as empty.",
|
|
2885
|
+
""
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
lines.push(
|
|
2889
|
+
"## Scope and limitations",
|
|
2890
|
+
"",
|
|
2891
|
+
...r.limitations.map((v) => `- ${md(v)}`),
|
|
2892
|
+
"",
|
|
2893
|
+
`Prisma AIRS CLI \xB7 ${md(r.schemaLabel ?? "Red Team")} report schema ${r.schemaVersion}`,
|
|
2894
|
+
""
|
|
2895
|
+
);
|
|
2896
|
+
return lines.join("\n");
|
|
2897
|
+
}
|
|
2898
|
+
var CSS = `
|
|
2899
|
+
:root{font:16px/1.6 system-ui,sans-serif;color:#193449;background:#f0f4f7}*{box-sizing:border-box}body{margin:0}header{background:#152e43;color:white;padding:3rem max(1rem,calc((100vw - 1160px)/2))}h1{font-size:clamp(1.8rem,4vw,3rem);line-height:1.15}h1,h2,h3,p,td{overflow-wrap:anywhere}main{max-width:1200px;margin:auto;padding:1rem}section,article{padding:1.4rem;background:white;border:1px solid #cddbe4;border-radius:10px;margin:1rem 0}.finding{border-left:5px solid #b08528}.finding[data-priority=attention]{border-left-color:#b1392d}.note{color:#536575;font-size:.9rem}.badge{border:1px solid;padding:.3rem .8rem;border-radius:30px;display:inline-block}nav,.controls{display:flex;gap:1rem;flex-wrap:wrap}button,select{font:inherit;padding:.4rem;border:1px solid #526778;border-radius:5px;background:white;color:#193449}a{color:#006b63}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;text-align:left}th,td{padding:.7rem;border-bottom:1px solid #d4dfe6;vertical-align:top}th{background:#edf3f7}caption{text-align:left}footer{padding:1rem;color:#536575}.js-only{display:none}.ready .js-only{display:flex}[hidden]{display:none!important}:focus-visible{outline:3px solid #ae6816;outline-offset:3px}.skip{position:absolute;left:-10000px}.skip:focus{left:1rem;top:1rem;background:white;padding:1rem}@media(max-width:600px){header{padding:1.5rem 1rem}section{padding:1rem}th,td{padding:.5rem}}@media print{body{background:white;font-size:10pt}header{background:white;color:black;padding:0}main{max-width:none;padding:0}nav,.controls,.skip{display:none!important}[hidden]{display:revert!important}.table-wrap{overflow:visible}section{border:0;padding:0}article{break-inside:avoid}}
|
|
2900
|
+
`;
|
|
2901
|
+
var JS = `'use strict';document.documentElement.classList.add('ready');document.getElementById('print').addEventListener('click',function(){window.print()});document.getElementById('priority').addEventListener('change',function(){var n=0;document.querySelectorAll('.finding').forEach(function(e){e.hidden=this.value!=='all'&&e.dataset.priority!==this.value;if(!e.hidden)n++},this);document.getElementById('count').textContent=n+' findings shown'});`;
|
|
2902
|
+
function renderEnvironmentReportHtml(r) {
|
|
2903
|
+
const hash = (v) => createHash("sha256").update(v).digest("base64");
|
|
2904
|
+
const policy = `default-src 'none'; script-src 'sha256-${hash(JS)}'; style-src 'sha256-${hash(CSS)}'; connect-src 'none'; base-uri 'none'; form-action 'none'`;
|
|
2905
|
+
const sections = tables(r).map(
|
|
2906
|
+
(t, i) => `<section id="table-${i}"><h2>${html(t.title)}</h2><p class="note">${html(t.note)}</p>${t.rows.length ? `<div class="table-wrap" role="region" aria-label="${html(t.title)}" tabindex="0"><table><caption>${t.rows.length} aggregate rows</caption><thead><tr>${t.headers.map((v) => `<th scope="col">${html(v)}</th>`).join("")}</tr></thead><tbody>${t.rows.map((row) => `<tr>${row.map((v) => `<td>${html(v)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>` : "<p>No rows collected; consult source completeness before interpreting this as empty.</p>"}</section>`
|
|
2907
|
+
).join("");
|
|
2908
|
+
return `<!doctype html>
|
|
2909
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${html(policy)}"><meta name="referrer" content="no-referrer"><title>${html(r.title)}</title><style>${CSS}</style></head><body><a class="skip" href="#main">Skip to report</a><header><p>${html(r.product)}</p><h1>${html(r.title)}</h1><span class="badge">${HEALTH[r.health]}</span><p>Collection: ${html(r.collectionStartedAt)} \u2192 ${html(r.generatedAt)}</p><p>${html(r.windowLabel ?? "Scan creation window (UTC)")}: ${html(r.window.start)} \u2192 ${html(r.window.end)}</p><p>Read-only evidence \xB7 Confidential environment metadata</p></header><main id="main"><nav aria-label="Report sections"><a href="#findings">Attention</a><a href="#table-0">Evidence tables</a><a href="#scope">Scope</a></nav><section id="findings"><h2>What needs attention</h2><div class="controls js-only"><label for="priority">Priority</label><select id="priority"><option value="all">All</option><option value="attention">Attention</option><option value="review">Review</option></select><span id="count" role="status"></span><button id="print" type="button">Print / save PDF</button></div>${r.findings.map((f) => `<article class="finding" data-priority="${html(f.priority)}"><h3>${html(f.title)}</h3><p>${html(f.evidence)}</p><p><strong>Next step:</strong> ${html(f.recommendation)}</p><p class="note">${html(f.source)}</p></article>`).join("") || "<p>No findings in available evidence; this does not establish absence of risk.</p>"}</section>${sections}<section id="scope"><h2>Scope and limitations</h2><ul>${r.limitations.map((v) => `<li>${html(v)}</li>`).join("")}</ul></section><footer>Prisma AIRS CLI \xB7 ${html(r.schemaLabel ?? "Red Team")} report schema ${r.schemaVersion} \xB7 Works offline and without JavaScript.</footer></main><script>${JS}</script></body></html>
|
|
2910
|
+
`;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2486
2913
|
// src/reports/io.ts
|
|
2487
2914
|
import { randomUUID } from "crypto";
|
|
2488
2915
|
import { link, open, unlink } from "fs/promises";
|
|
@@ -2505,7 +2932,7 @@ async function writeReportFile(path2, content) {
|
|
|
2505
2932
|
var DataError = class extends Error {
|
|
2506
2933
|
};
|
|
2507
2934
|
var label = (value) => typeof value === "string" ? value.replace(/[\p{Cc}\p{Cf}]/gu, " ").slice(0, 120) : "Unknown";
|
|
2508
|
-
function
|
|
2935
|
+
function count2(value) {
|
|
2509
2936
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
2510
2937
|
throw new DataError("A required counter is missing or invalid.");
|
|
2511
2938
|
return value;
|
|
@@ -2581,7 +3008,7 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2581
3008
|
const r = await read(rows.length);
|
|
2582
3009
|
s.pages++;
|
|
2583
3010
|
if (!Array.isArray(r.data)) throw new DataError("Missing inventory array.");
|
|
2584
|
-
const reported =
|
|
3011
|
+
const reported = count2(r.pagination.total_items);
|
|
2585
3012
|
total2 ??= reported;
|
|
2586
3013
|
if (total2 !== reported) throw new DataError("Inventory changed during pagination.");
|
|
2587
3014
|
if (r.data.length > 15) throw new DataError("Page exceeds requested size.");
|
|
@@ -2615,7 +3042,7 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2615
3042
|
}
|
|
2616
3043
|
await source2("Overview", "getDashboardOverview()", "Current dashboard snapshot", async (s) => {
|
|
2617
3044
|
const r = await client.getDashboardOverview();
|
|
2618
|
-
const total2 =
|
|
3045
|
+
const total2 = count2(r.total_targets);
|
|
2619
3046
|
table("Dashboard overview", s.window, ["Metric", "Count"], [["Targets", total2]]);
|
|
2620
3047
|
s.records = 1;
|
|
2621
3048
|
s.pages = 1;
|
|
@@ -2627,11 +3054,11 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2627
3054
|
async (s) => {
|
|
2628
3055
|
const r = await client.getScanStatistics();
|
|
2629
3056
|
const rows = [
|
|
2630
|
-
["Scans",
|
|
2631
|
-
["Targets scanned",
|
|
3057
|
+
["Scans", count2(r.total_scans)],
|
|
3058
|
+
["Targets scanned", count2(r.targets_scanned)]
|
|
2632
3059
|
];
|
|
2633
3060
|
for (const risk of r.risk_profile ?? []) {
|
|
2634
|
-
const n =
|
|
3061
|
+
const n = count2(risk.total);
|
|
2635
3062
|
rows.push([`Risk: ${label(risk.risk_rating)}`, n]);
|
|
2636
3063
|
if (/^(high|critical)$/i.test(risk.risk_rating) && n > 0)
|
|
2637
3064
|
finding(
|
|
@@ -2643,7 +3070,7 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2643
3070
|
);
|
|
2644
3071
|
}
|
|
2645
3072
|
for (const status of r.scan_status ?? [])
|
|
2646
|
-
rows.push([`Status: ${label(status.name)}`,
|
|
3073
|
+
rows.push([`Status: ${label(status.name)}`, count2(status.count)]);
|
|
2647
3074
|
table("Dashboard scan statistics", s.window, ["Metric", "Count"], rows);
|
|
2648
3075
|
s.records = 1;
|
|
2649
3076
|
s.pages = 1;
|
|
@@ -2667,8 +3094,8 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2667
3094
|
const rows = [];
|
|
2668
3095
|
for (const kind of ["static", "dynamic", "custom"]) {
|
|
2669
3096
|
const q = r[kind];
|
|
2670
|
-
const allocated =
|
|
2671
|
-
const consumed =
|
|
3097
|
+
const allocated = count2(q.allocated);
|
|
3098
|
+
const consumed = count2(q.consumed);
|
|
2672
3099
|
if (typeof q.unlimited !== "boolean") throw new DataError("Missing unlimited quota flag.");
|
|
2673
3100
|
rows.push([
|
|
2674
3101
|
kind,
|
|
@@ -2750,8 +3177,8 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2750
3177
|
"Current channel snapshot",
|
|
2751
3178
|
async (s) => {
|
|
2752
3179
|
const r = await client.networkBroker.getChannelStats();
|
|
2753
|
-
const total2 =
|
|
2754
|
-
const online =
|
|
3180
|
+
const total2 = count2(r.total_channels);
|
|
3181
|
+
const online = count2(r.online_channels);
|
|
2755
3182
|
if (online > total2) throw new DataError("Online channel count exceeds total.");
|
|
2756
3183
|
table(
|
|
2757
3184
|
"Network broker",
|
|
@@ -2797,103 +3224,11 @@ async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
|
2797
3224
|
}
|
|
2798
3225
|
|
|
2799
3226
|
// src/reports/redteam-render.ts
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
attention: "Attention required",
|
|
2803
|
-
review: "Review required",
|
|
2804
|
-
"no-findings": "No findings in available evidence",
|
|
2805
|
-
unknown: "Unable to assess"
|
|
2806
|
-
};
|
|
2807
|
-
var html = (v) => String(v ?? "Unknown").replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
2808
|
-
var md = (v) => String(v ?? "Unknown").replace(
|
|
2809
|
-
/[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
|
|
2810
|
-
(c) => /[\r\n]/.test(c) ? " " : `&#${c.charCodeAt(0)};`
|
|
2811
|
-
);
|
|
2812
|
-
function tables(r) {
|
|
2813
|
-
return [
|
|
2814
|
-
...r.tables,
|
|
2815
|
-
{
|
|
2816
|
-
title: "Evidence and completeness",
|
|
2817
|
-
note: "Complete means the source was collected within its budget, not that the environment is secure. Unavailable is not zero.",
|
|
2818
|
-
headers: ["Source", "SDK method", "Window", "Status", "Records", "Pages", "Notes"],
|
|
2819
|
-
rows: r.sources.map((s) => [
|
|
2820
|
-
s.name,
|
|
2821
|
-
s.method,
|
|
2822
|
-
s.window,
|
|
2823
|
-
s.status,
|
|
2824
|
-
s.records,
|
|
2825
|
-
s.pages,
|
|
2826
|
-
s.notes.join(" ") || "Collection completed."
|
|
2827
|
-
])
|
|
2828
|
-
}
|
|
2829
|
-
];
|
|
2830
|
-
}
|
|
2831
|
-
function renderRedTeamReportMarkdown(r) {
|
|
2832
|
-
const lines = [
|
|
2833
|
-
`# ${md(r.title)}`,
|
|
2834
|
-
"",
|
|
2835
|
-
`**${r.product}**`,
|
|
2836
|
-
"",
|
|
2837
|
-
`Assessment: **${HEALTH[r.health]}**`,
|
|
2838
|
-
"",
|
|
2839
|
-
`Collection: ${r.collectionStartedAt} \u2192 ${r.generatedAt}`,
|
|
2840
|
-
"",
|
|
2841
|
-
`Scan creation window (UTC): ${r.window.start} \u2192 ${r.window.end}`,
|
|
2842
|
-
"",
|
|
2843
|
-
"## What needs attention",
|
|
2844
|
-
""
|
|
2845
|
-
];
|
|
2846
|
-
for (const f of r.findings)
|
|
2847
|
-
lines.push(
|
|
2848
|
-
`### ${f.priority.toUpperCase()}: ${md(f.title)}`,
|
|
2849
|
-
"",
|
|
2850
|
-
`Evidence: ${md(f.evidence)}`,
|
|
2851
|
-
"",
|
|
2852
|
-
`Next step: ${md(f.recommendation)}`,
|
|
2853
|
-
"",
|
|
2854
|
-
`Source: ${md(f.source)}`,
|
|
2855
|
-
""
|
|
2856
|
-
);
|
|
2857
|
-
if (!r.findings.length)
|
|
2858
|
-
lines.push("No findings in available evidence; this does not establish absence of risk.", "");
|
|
2859
|
-
for (const t of tables(r)) {
|
|
2860
|
-
lines.push(`## ${md(t.title)}`, "", md(t.note), "");
|
|
2861
|
-
if (t.rows.length)
|
|
2862
|
-
lines.push(
|
|
2863
|
-
`| ${t.headers.map(md).join(" | ")} |`,
|
|
2864
|
-
`| ${t.headers.map(() => "---").join(" | ")} |`,
|
|
2865
|
-
...t.rows.map((row) => `| ${row.map(md).join(" | ")} |`),
|
|
2866
|
-
""
|
|
2867
|
-
);
|
|
2868
|
-
else
|
|
2869
|
-
lines.push(
|
|
2870
|
-
"No rows collected; consult source completeness before interpreting this as empty.",
|
|
2871
|
-
""
|
|
2872
|
-
);
|
|
2873
|
-
}
|
|
2874
|
-
lines.push(
|
|
2875
|
-
"## Scope and limitations",
|
|
2876
|
-
"",
|
|
2877
|
-
...r.limitations.map((v) => `- ${md(v)}`),
|
|
2878
|
-
"",
|
|
2879
|
-
`Prisma AIRS CLI \xB7 Red Team report schema ${r.schemaVersion}`,
|
|
2880
|
-
""
|
|
2881
|
-
);
|
|
2882
|
-
return lines.join("\n");
|
|
3227
|
+
function renderRedTeamReportMarkdown(report) {
|
|
3228
|
+
return renderEnvironmentReportMarkdown(report);
|
|
2883
3229
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
`;
|
|
2887
|
-
var JS = `'use strict';document.documentElement.classList.add('ready');document.getElementById('print').addEventListener('click',function(){window.print()});document.getElementById('priority').addEventListener('change',function(){var n=0;document.querySelectorAll('.finding').forEach(function(e){e.hidden=this.value!=='all'&&e.dataset.priority!==this.value;if(!e.hidden)n++},this);document.getElementById('count').textContent=n+' findings shown'});`;
|
|
2888
|
-
function renderRedTeamReportHtml(r) {
|
|
2889
|
-
const hash = (v) => createHash("sha256").update(v).digest("base64");
|
|
2890
|
-
const policy = `default-src 'none'; script-src 'sha256-${hash(JS)}'; style-src 'sha256-${hash(CSS)}'; connect-src 'none'; base-uri 'none'; form-action 'none'`;
|
|
2891
|
-
const sections = tables(r).map(
|
|
2892
|
-
(t, i) => `<section id="table-${i}"><h2>${html(t.title)}</h2><p class="note">${html(t.note)}</p>${t.rows.length ? `<div class="table-wrap" role="region" aria-label="${html(t.title)}" tabindex="0"><table><caption>${t.rows.length} aggregate rows</caption><thead><tr>${t.headers.map((v) => `<th scope="col">${html(v)}</th>`).join("")}</tr></thead><tbody>${t.rows.map((row) => `<tr>${row.map((v) => `<td>${html(v)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>` : "<p>No rows collected; consult source completeness before interpreting this as empty.</p>"}</section>`
|
|
2893
|
-
).join("");
|
|
2894
|
-
return `<!doctype html>
|
|
2895
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${html(policy)}"><meta name="referrer" content="no-referrer"><title>${html(r.title)}</title><style>${CSS}</style></head><body><a class="skip" href="#main">Skip to report</a><header><p>${r.product}</p><h1>${html(r.title)}</h1><span class="badge">${HEALTH[r.health]}</span><p>Collection: ${html(r.collectionStartedAt)} \u2192 ${html(r.generatedAt)}</p><p>Scan creation window (UTC): ${html(r.window.start)} \u2192 ${html(r.window.end)}</p><p>Read-only evidence \xB7 Confidential environment metadata</p></header><main id="main"><nav aria-label="Report sections"><a href="#findings">Attention</a><a href="#table-0">Evidence tables</a><a href="#scope">Scope</a></nav><section id="findings"><h2>What needs attention</h2><div class="controls js-only"><label for="priority">Priority</label><select id="priority"><option value="all">All</option><option value="attention">Attention</option><option value="review">Review</option></select><span id="count" role="status"></span><button id="print" type="button">Print / save PDF</button></div>${r.findings.map((f) => `<article class="finding" data-priority="${html(f.priority)}"><h3>${html(f.title)}</h3><p>${html(f.evidence)}</p><p><strong>Next step:</strong> ${html(f.recommendation)}</p><p class="note">${html(f.source)}</p></article>`).join("") || "<p>No findings in available evidence; this does not establish absence of risk.</p>"}</section>${sections}<section id="scope"><h2>Scope and limitations</h2><ul>${r.limitations.map((v) => `<li>${html(v)}</li>`).join("")}</ul></section><footer>Prisma AIRS CLI \xB7 Red Team report schema ${r.schemaVersion} \xB7 Works offline and without JavaScript.</footer></main><script>${JS}</script></body></html>
|
|
2896
|
-
`;
|
|
3230
|
+
function renderRedTeamReportHtml(report) {
|
|
3231
|
+
return renderEnvironmentReportHtml(report);
|
|
2897
3232
|
}
|
|
2898
3233
|
|
|
2899
3234
|
// src/reports/render.ts
|
|
@@ -3176,7 +3511,7 @@ function safeFailure(error) {
|
|
|
3176
3511
|
function label2(value) {
|
|
3177
3512
|
return (value?.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim() || "Unknown").slice(0, 240);
|
|
3178
3513
|
}
|
|
3179
|
-
function
|
|
3514
|
+
function count3(value) {
|
|
3180
3515
|
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
3181
3516
|
}
|
|
3182
3517
|
async function collect(source2, maxPages, read, identity) {
|
|
@@ -3226,7 +3561,7 @@ function inventoryNext(next, size, offset) {
|
|
|
3226
3561
|
function total(values) {
|
|
3227
3562
|
if (values.some((value) => value === null)) return null;
|
|
3228
3563
|
const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
3229
|
-
return
|
|
3564
|
+
return count3(result);
|
|
3230
3565
|
}
|
|
3231
3566
|
function distribution(rows) {
|
|
3232
3567
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -3238,11 +3573,11 @@ function distribution(rows) {
|
|
|
3238
3573
|
}
|
|
3239
3574
|
function severity(value) {
|
|
3240
3575
|
return {
|
|
3241
|
-
critical:
|
|
3242
|
-
high:
|
|
3243
|
-
medium:
|
|
3244
|
-
low:
|
|
3245
|
-
total:
|
|
3576
|
+
critical: count3(value.critical),
|
|
3577
|
+
high: count3(value.high),
|
|
3578
|
+
medium: count3(value.medium),
|
|
3579
|
+
low: count3(value.low),
|
|
3580
|
+
total: count3(value.total)
|
|
3246
3581
|
};
|
|
3247
3582
|
}
|
|
3248
3583
|
async function collectRuntimeDailyReport(client, options = {}) {
|
|
@@ -3305,7 +3640,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3305
3640
|
offset
|
|
3306
3641
|
});
|
|
3307
3642
|
const size = page.items?.length ?? 0;
|
|
3308
|
-
const totalItems =
|
|
3643
|
+
const totalItems = count3(page.pagination?.total_items);
|
|
3309
3644
|
if (totalItems !== null) {
|
|
3310
3645
|
if (expectedAppTotal !== void 0 && totalItems !== expectedAppTotal)
|
|
3311
3646
|
throw new ReportDataError(
|
|
@@ -3357,7 +3692,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3357
3692
|
async (offset) => {
|
|
3358
3693
|
const page = await client.dashboard.sessionsOverview({ ...daily, limit: 25, offset });
|
|
3359
3694
|
const size = page.items?.length ?? 0;
|
|
3360
|
-
const totalItems =
|
|
3695
|
+
const totalItems = count3(page.pagination?.total_items);
|
|
3361
3696
|
if (totalItems === null || page.pagination?.skip !== offset || page.pagination?.limit !== 25)
|
|
3362
3697
|
throw new ReportDataError("Missing or mismatched session pagination metadata.");
|
|
3363
3698
|
if (expectedSessionTotal !== void 0 && expectedSessionTotal !== totalItems)
|
|
@@ -3414,8 +3749,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3414
3749
|
const appIds = new Set(registered.map((app) => app.customer_appId));
|
|
3415
3750
|
const reportApps = applications.map((app) => ({
|
|
3416
3751
|
name: label2(app.name),
|
|
3417
|
-
sessions:
|
|
3418
|
-
violatingSessions:
|
|
3752
|
+
sessions: count3(app.sessions_total),
|
|
3753
|
+
violatingSessions: count3(app.sessions_violated),
|
|
3419
3754
|
registered: app.id && appIds.has(app.id) ? true : appSource.status === "complete" && app.id ? false : null
|
|
3420
3755
|
})).sort(
|
|
3421
3756
|
(a, b) => (b.violatingSessions ?? -1) - (a.violatingSessions ?? -1) || a.name.localeCompare(b.name)
|
|
@@ -3492,7 +3827,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3492
3827
|
);
|
|
3493
3828
|
return {
|
|
3494
3829
|
name,
|
|
3495
|
-
revision:
|
|
3830
|
+
revision: count3(profile.revision),
|
|
3496
3831
|
active: profile.active ?? null,
|
|
3497
3832
|
modified: profile.last_modified_ts && Number.isFinite(Date.parse(profile.last_modified_ts)) ? new Date(profile.last_modified_ts).toISOString() : null,
|
|
3498
3833
|
timeoutActions,
|
|
@@ -3540,8 +3875,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3540
3875
|
"Review these sessions in SCM and check integration error handling. This is not a tenant-wide failure rate.",
|
|
3541
3876
|
sessionSource.name
|
|
3542
3877
|
);
|
|
3543
|
-
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) =>
|
|
3544
|
-
const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) =>
|
|
3878
|
+
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count3(bucket.total)));
|
|
3879
|
+
const chartViolatingSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) => count3(bucket.violating)));
|
|
3545
3880
|
if (chartSessions !== null && sessionSource.status === "complete" && chartSessions !== rawSessions.length)
|
|
3546
3881
|
add(
|
|
3547
3882
|
"review",
|
|
@@ -3552,7 +3887,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3552
3887
|
);
|
|
3553
3888
|
const urgentViolations = total(
|
|
3554
3889
|
trend.map(
|
|
3555
|
-
(bucket) => total([
|
|
3890
|
+
(bucket) => total([count3(bucket.violation_breakdown.critical), count3(bucket.violation_breakdown.high)])
|
|
3556
3891
|
)
|
|
3557
3892
|
);
|
|
3558
3893
|
if ((urgentViolations ?? 0) > 0)
|
|
@@ -3606,17 +3941,17 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3606
3941
|
violatingSessions: chartViolatingSessions,
|
|
3607
3942
|
buckets: chart.map((bucket) => ({
|
|
3608
3943
|
time: label2(bucket.time),
|
|
3609
|
-
sessions:
|
|
3610
|
-
violatingSessions:
|
|
3944
|
+
sessions: count3(bucket.total),
|
|
3945
|
+
violatingSessions: count3(bucket.violating),
|
|
3611
3946
|
violations: severity(bucket.violation_breakdown)
|
|
3612
3947
|
}))
|
|
3613
3948
|
},
|
|
3614
3949
|
topApplications: rankings.map((app) => ({
|
|
3615
3950
|
name: label2(app.name),
|
|
3616
|
-
violations:
|
|
3951
|
+
violations: count3(app.total_violations),
|
|
3617
3952
|
detectors: app.policy_violations.map((detector) => ({
|
|
3618
3953
|
name: label2(detector.detection_type),
|
|
3619
|
-
count:
|
|
3954
|
+
count: count3(detector.total)
|
|
3620
3955
|
}))
|
|
3621
3956
|
})),
|
|
3622
3957
|
violationTrend: trend.map((bucket) => ({
|
|
@@ -3662,6 +3997,9 @@ export {
|
|
|
3662
3997
|
validateTopic,
|
|
3663
3998
|
computeMetrics,
|
|
3664
3999
|
computeCategoryBreakdown,
|
|
4000
|
+
collectGatewayEnvironmentReport,
|
|
4001
|
+
renderEnvironmentReportMarkdown,
|
|
4002
|
+
renderEnvironmentReportHtml,
|
|
3665
4003
|
writeReportFile,
|
|
3666
4004
|
collectRedTeamEnvironmentReport,
|
|
3667
4005
|
renderRedTeamReportMarkdown,
|