@cdot65/prisma-airs-cli 5.0.1 → 5.2.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 -0
- package/dist/{chunk-IRLIFMGQ.js → chunk-7Z76VCB3.js} +797 -61
- package/dist/cli/index.js +278 -66
- package/dist/index.d.ts +87 -5
- package/dist/index.js +13 -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";
|
|
@@ -2501,15 +2928,318 @@ async function writeReportFile(path2, content) {
|
|
|
2501
2928
|
}
|
|
2502
2929
|
}
|
|
2503
2930
|
|
|
2931
|
+
// src/reports/redteam.ts
|
|
2932
|
+
var DataError = class extends Error {
|
|
2933
|
+
};
|
|
2934
|
+
var label = (value) => typeof value === "string" ? value.replace(/[\p{Cc}\p{Cf}]/gu, " ").slice(0, 120) : "Unknown";
|
|
2935
|
+
function count2(value) {
|
|
2936
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
2937
|
+
throw new DataError("A required counter is missing or invalid.");
|
|
2938
|
+
return value;
|
|
2939
|
+
}
|
|
2940
|
+
function failure(error) {
|
|
2941
|
+
if (error instanceof DataError) return error.message;
|
|
2942
|
+
const code = error?.statusCode;
|
|
2943
|
+
return typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599 ? `HTTP ${code}: source unavailable; check credentials, service availability and SDK compatibility.` : "Source request or validation failed. No upstream error body is included.";
|
|
2944
|
+
}
|
|
2945
|
+
async function collectRedTeamEnvironmentReport(client, options = {}) {
|
|
2946
|
+
const maxPages = options.maxPages ?? 40;
|
|
2947
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 100)
|
|
2948
|
+
throw new Error("maxPages must be an integer from 1 to 100");
|
|
2949
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2950
|
+
const end = now();
|
|
2951
|
+
const report = {
|
|
2952
|
+
schemaVersion: 1,
|
|
2953
|
+
product: "Prisma AIRS AI Red Teaming",
|
|
2954
|
+
title: options.title ?? "Red Team environment report",
|
|
2955
|
+
collectionStartedAt: end.toISOString(),
|
|
2956
|
+
generatedAt: "",
|
|
2957
|
+
window: { start: new Date(end.getTime() - 864e5).toISOString(), end: end.toISOString() },
|
|
2958
|
+
health: "unknown",
|
|
2959
|
+
sources: [],
|
|
2960
|
+
findings: [],
|
|
2961
|
+
tables: [],
|
|
2962
|
+
limitations: [
|
|
2963
|
+
"This is a read-only snapshot, not a security certification, availability SLA or numerical health score.",
|
|
2964
|
+
"Only scan creation timestamps are filtered locally to the preceding 24 hours. Current scan status is not a history of status changes.",
|
|
2965
|
+
"Dashboard statistics use server defaults with an unverified measurement window. They are not daily totals and need not match the paginated scan inventory.",
|
|
2966
|
+
"Targets and adapters are current configuration inventories, not configuration change logs. Broker channels not online may be intentionally idle; no outage is inferred.",
|
|
2967
|
+
"Risk categories are server-reported. Quota consumption is not reset or reinterpreted as daily usage; unlimited zero allocation is not exhaustion.",
|
|
2968
|
+
"Partial sources describe collected records only. Missing data is unknown, never zero. Independently read pages are not an atomic snapshot.",
|
|
2969
|
+
"Only aggregate counters and category labels are included. Target names, IDs, connection settings, scripts, variables, prompts and scan content are excluded."
|
|
2970
|
+
]
|
|
2971
|
+
};
|
|
2972
|
+
function finding(priority, title, evidence, recommendation, source3) {
|
|
2973
|
+
report.findings.push({ priority, title, evidence, recommendation, source: source3 });
|
|
2974
|
+
}
|
|
2975
|
+
async function source2(name, method, window, read) {
|
|
2976
|
+
const s = {
|
|
2977
|
+
name,
|
|
2978
|
+
method,
|
|
2979
|
+
window,
|
|
2980
|
+
status: "unavailable",
|
|
2981
|
+
records: 0,
|
|
2982
|
+
pages: 0,
|
|
2983
|
+
notes: []
|
|
2984
|
+
};
|
|
2985
|
+
report.sources.push(s);
|
|
2986
|
+
try {
|
|
2987
|
+
await read(s);
|
|
2988
|
+
if (s.status !== "partial") s.status = "complete";
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
s.status = s.records > 0 ? "partial" : "unavailable";
|
|
2991
|
+
s.notes.push(failure(error));
|
|
2992
|
+
}
|
|
2993
|
+
if (s.status !== "complete")
|
|
2994
|
+
finding(
|
|
2995
|
+
"review",
|
|
2996
|
+
`${name} incomplete`,
|
|
2997
|
+
s.notes.join(" "),
|
|
2998
|
+
"Resolve the source error or increase the page budget, then regenerate.",
|
|
2999
|
+
name
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
async function pages(s, read) {
|
|
3003
|
+
const rows = [];
|
|
3004
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3005
|
+
let total2;
|
|
3006
|
+
try {
|
|
3007
|
+
for (let page = 0; page < maxPages; page++) {
|
|
3008
|
+
const r = await read(rows.length);
|
|
3009
|
+
s.pages++;
|
|
3010
|
+
if (!Array.isArray(r.data)) throw new DataError("Missing inventory array.");
|
|
3011
|
+
const reported = count2(r.pagination.total_items);
|
|
3012
|
+
total2 ??= reported;
|
|
3013
|
+
if (total2 !== reported) throw new DataError("Inventory changed during pagination.");
|
|
3014
|
+
if (r.data.length > 15) throw new DataError("Page exceeds requested size.");
|
|
3015
|
+
for (const row of r.data) {
|
|
3016
|
+
if (!row.uuid || seen.has(row.uuid))
|
|
3017
|
+
throw new DataError("Missing or repeated identity; pagination stopped.");
|
|
3018
|
+
seen.add(row.uuid);
|
|
3019
|
+
rows.push(row);
|
|
3020
|
+
s.records = rows.length;
|
|
3021
|
+
}
|
|
3022
|
+
if (rows.length > total2) throw new DataError("Inventory exceeds reported total.");
|
|
3023
|
+
if (rows.length === total2) return rows;
|
|
3024
|
+
if (!r.data.length) throw new DataError("Premature empty page.");
|
|
3025
|
+
}
|
|
3026
|
+
throw new DataError(`Reached the ${maxPages}-page budget; inventory is incomplete.`);
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
if (!rows.length) throw error;
|
|
3029
|
+
s.status = "partial";
|
|
3030
|
+
s.notes.push(failure(error));
|
|
3031
|
+
return rows;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
const table = (title, note, headers, rows) => report.tables.push({ title, note, headers, rows });
|
|
3035
|
+
function groups(values) {
|
|
3036
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3037
|
+
for (const v of values) {
|
|
3038
|
+
const key = label(v);
|
|
3039
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3040
|
+
}
|
|
3041
|
+
return [...counts].sort(([a], [b]) => a.localeCompare(b));
|
|
3042
|
+
}
|
|
3043
|
+
await source2("Overview", "getDashboardOverview()", "Current dashboard snapshot", async (s) => {
|
|
3044
|
+
const r = await client.getDashboardOverview();
|
|
3045
|
+
const total2 = count2(r.total_targets);
|
|
3046
|
+
table("Dashboard overview", s.window, ["Metric", "Count"], [["Targets", total2]]);
|
|
3047
|
+
s.records = 1;
|
|
3048
|
+
s.pages = 1;
|
|
3049
|
+
});
|
|
3050
|
+
await source2(
|
|
3051
|
+
"Statistics",
|
|
3052
|
+
"getScanStatistics()",
|
|
3053
|
+
"Server-default window (unverified)",
|
|
3054
|
+
async (s) => {
|
|
3055
|
+
const r = await client.getScanStatistics();
|
|
3056
|
+
const rows = [
|
|
3057
|
+
["Scans", count2(r.total_scans)],
|
|
3058
|
+
["Targets scanned", count2(r.targets_scanned)]
|
|
3059
|
+
];
|
|
3060
|
+
for (const risk of r.risk_profile ?? []) {
|
|
3061
|
+
const n = count2(risk.total);
|
|
3062
|
+
rows.push([`Risk: ${label(risk.risk_rating)}`, n]);
|
|
3063
|
+
if (/^(high|critical)$/i.test(risk.risk_rating) && n > 0)
|
|
3064
|
+
finding(
|
|
3065
|
+
"attention",
|
|
3066
|
+
"Elevated risk reported",
|
|
3067
|
+
`${n} in the ${label(risk.risk_rating)} risk category.`,
|
|
3068
|
+
"Review affected targets in Red Team; the statistics window is server-defined.",
|
|
3069
|
+
s.name
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
for (const status of r.scan_status ?? [])
|
|
3073
|
+
rows.push([`Status: ${label(status.name)}`, count2(status.count)]);
|
|
3074
|
+
table("Dashboard scan statistics", s.window, ["Metric", "Count"], rows);
|
|
3075
|
+
s.records = 1;
|
|
3076
|
+
s.pages = 1;
|
|
3077
|
+
}
|
|
3078
|
+
);
|
|
3079
|
+
await source2("Targets", "targets.list()", "Current paginated inventory", async (s) => {
|
|
3080
|
+
const rows = await pages(s, (skip) => client.targets.list({ skip, limit: 15 }));
|
|
3081
|
+
table(
|
|
3082
|
+
"Target inventory",
|
|
3083
|
+
"Collected targets by type; consult source completeness.",
|
|
3084
|
+
["Type", "Count"],
|
|
3085
|
+
groups(rows.map((r) => r.target_type))
|
|
3086
|
+
);
|
|
3087
|
+
});
|
|
3088
|
+
await source2("Quota", "getQuotaSummary() \u2014 GET", "Current quota snapshot", async (s) => {
|
|
3089
|
+
if (!client.getQuotaSummary)
|
|
3090
|
+
throw new DataError(
|
|
3091
|
+
"Installed SDK lacks getQuotaSummary(); use the locally updated SDK. POST is not used as a fallback."
|
|
3092
|
+
);
|
|
3093
|
+
const r = await client.getQuotaSummary();
|
|
3094
|
+
const rows = [];
|
|
3095
|
+
for (const kind of ["static", "dynamic", "custom"]) {
|
|
3096
|
+
const q = r[kind];
|
|
3097
|
+
const allocated = count2(q.allocated);
|
|
3098
|
+
const consumed = count2(q.consumed);
|
|
3099
|
+
if (typeof q.unlimited !== "boolean") throw new DataError("Missing unlimited quota flag.");
|
|
3100
|
+
rows.push([
|
|
3101
|
+
kind,
|
|
3102
|
+
allocated,
|
|
3103
|
+
consumed,
|
|
3104
|
+
q.unlimited ? "Unlimited" : String(Math.max(0, allocated - consumed))
|
|
3105
|
+
]);
|
|
3106
|
+
if (!q.unlimited && consumed >= allocated)
|
|
3107
|
+
finding(
|
|
3108
|
+
"attention",
|
|
3109
|
+
`${kind} quota has no remaining allocation`,
|
|
3110
|
+
`${consumed} consumed; ${allocated} allocated.`,
|
|
3111
|
+
"Review quota allocation before scheduling more scans.",
|
|
3112
|
+
s.name
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
table(
|
|
3116
|
+
"Quota",
|
|
3117
|
+
"Remaining is shown only for finite quotas; these are not daily usage counters.",
|
|
3118
|
+
["Type", "Allocated", "Consumed", "Remaining"],
|
|
3119
|
+
rows
|
|
3120
|
+
);
|
|
3121
|
+
s.records = 3;
|
|
3122
|
+
s.pages = 1;
|
|
3123
|
+
});
|
|
3124
|
+
await source2(
|
|
3125
|
+
"Scans",
|
|
3126
|
+
"scans.list()",
|
|
3127
|
+
"Paginated inventory; local 24-hour creation filter",
|
|
3128
|
+
async (s) => {
|
|
3129
|
+
const rows = await pages(s, (skip) => client.scans.list({ skip, limit: 15 }));
|
|
3130
|
+
let unknown = 0;
|
|
3131
|
+
const recent = rows.filter((r) => {
|
|
3132
|
+
const t = typeof r.created_at === "string" && /(?:Z|[+-]\d{2}:\d{2})$/i.test(r.created_at) ? Date.parse(r.created_at) : Number.NaN;
|
|
3133
|
+
if (!Number.isFinite(t) || t > end.getTime()) {
|
|
3134
|
+
unknown++;
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
return t >= end.getTime() - 864e5;
|
|
3138
|
+
});
|
|
3139
|
+
table(
|
|
3140
|
+
"Scan inventory and daily creation activity",
|
|
3141
|
+
"Daily count covers collected records created in the stated UTC window, not scans completed or attacks executed.",
|
|
3142
|
+
["Metric", "Count"],
|
|
3143
|
+
[
|
|
3144
|
+
["Collected scans", rows.length],
|
|
3145
|
+
["Created in preceding 24 hours", recent.length],
|
|
3146
|
+
["Unusable or future creation timestamps", unknown]
|
|
3147
|
+
]
|
|
3148
|
+
);
|
|
3149
|
+
table(
|
|
3150
|
+
"Current status of collected scans",
|
|
3151
|
+
"All collected scans, independent of creation window.",
|
|
3152
|
+
["Status", "Count"],
|
|
3153
|
+
groups(rows.map((r) => r.status))
|
|
3154
|
+
);
|
|
3155
|
+
const failed = rows.filter((r) => /^(failed|error)$/i.test(r.status ?? "")).length;
|
|
3156
|
+
if (failed)
|
|
3157
|
+
finding(
|
|
3158
|
+
"attention",
|
|
3159
|
+
"Scans require failure review",
|
|
3160
|
+
`${failed} collected scans currently report failed/error status.`,
|
|
3161
|
+
"Inspect individual scan reports and error logs before rerunning; no reruns are performed here.",
|
|
3162
|
+
s.name
|
|
3163
|
+
);
|
|
3164
|
+
if (unknown)
|
|
3165
|
+
finding(
|
|
3166
|
+
"review",
|
|
3167
|
+
"Daily activity has timestamp gaps",
|
|
3168
|
+
`${unknown} records could not be assigned to the daily window.`,
|
|
3169
|
+
"Review source timestamps; do not interpret the daily count as complete.",
|
|
3170
|
+
s.name
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
);
|
|
3174
|
+
await source2(
|
|
3175
|
+
"Network broker",
|
|
3176
|
+
"networkBroker.getChannelStats()",
|
|
3177
|
+
"Current channel snapshot",
|
|
3178
|
+
async (s) => {
|
|
3179
|
+
const r = await client.networkBroker.getChannelStats();
|
|
3180
|
+
const total2 = count2(r.total_channels);
|
|
3181
|
+
const online = count2(r.online_channels);
|
|
3182
|
+
if (online > total2) throw new DataError("Online channel count exceeds total.");
|
|
3183
|
+
table(
|
|
3184
|
+
"Network broker",
|
|
3185
|
+
"Not-online channels may be intentionally idle; this does not establish an outage.",
|
|
3186
|
+
["Metric", "Count"],
|
|
3187
|
+
[
|
|
3188
|
+
["Configured channels", total2],
|
|
3189
|
+
["Online channels", online]
|
|
3190
|
+
]
|
|
3191
|
+
);
|
|
3192
|
+
if (total2 > online)
|
|
3193
|
+
finding(
|
|
3194
|
+
"review",
|
|
3195
|
+
"Confirm expected broker connectivity",
|
|
3196
|
+
`${online} of ${total2} configured channels are online.`,
|
|
3197
|
+
"Compare with intended channel schedules before investigating connectivity.",
|
|
3198
|
+
s.name
|
|
3199
|
+
);
|
|
3200
|
+
s.records = 1;
|
|
3201
|
+
s.pages = 1;
|
|
3202
|
+
}
|
|
3203
|
+
);
|
|
3204
|
+
await source2(
|
|
3205
|
+
"Adapters",
|
|
3206
|
+
"adapters.list({ include_target_count: true })",
|
|
3207
|
+
"Current paginated configuration",
|
|
3208
|
+
async (s) => {
|
|
3209
|
+
const rows = await pages(
|
|
3210
|
+
s,
|
|
3211
|
+
(skip) => client.adapters.list({ skip, limit: 15, include_target_count: true })
|
|
3212
|
+
);
|
|
3213
|
+
table(
|
|
3214
|
+
"Adapter inventory",
|
|
3215
|
+
"Configuration status only; scripts and variables are excluded.",
|
|
3216
|
+
["Status", "Count"],
|
|
3217
|
+
groups(rows.map((r) => r.status))
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
);
|
|
3221
|
+
report.health = report.sources.every((s) => s.status === "unavailable") ? "unknown" : report.findings.some((f) => f.priority === "attention") ? "attention" : report.findings.length ? "review" : "no-findings";
|
|
3222
|
+
report.generatedAt = now().toISOString();
|
|
3223
|
+
return report;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// src/reports/redteam-render.ts
|
|
3227
|
+
function renderRedTeamReportMarkdown(report) {
|
|
3228
|
+
return renderEnvironmentReportMarkdown(report);
|
|
3229
|
+
}
|
|
3230
|
+
function renderRedTeamReportHtml(report) {
|
|
3231
|
+
return renderEnvironmentReportHtml(report);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
2504
3234
|
// src/reports/render.ts
|
|
2505
|
-
import { createHash } from "crypto";
|
|
2506
|
-
var
|
|
3235
|
+
import { createHash as createHash2 } from "crypto";
|
|
3236
|
+
var HEALTH2 = {
|
|
2507
3237
|
attention: "Attention required",
|
|
2508
3238
|
review: "Review required",
|
|
2509
3239
|
"no-findings": "No findings in available evidence",
|
|
2510
3240
|
unknown: "Unable to assess"
|
|
2511
3241
|
};
|
|
2512
|
-
function
|
|
3242
|
+
function html2(value) {
|
|
2513
3243
|
return String(value ?? "Unknown").replace(
|
|
2514
3244
|
/[&<>"']/g,
|
|
2515
3245
|
(char) => ({
|
|
@@ -2521,7 +3251,7 @@ function html(value) {
|
|
|
2521
3251
|
})[char] ?? char
|
|
2522
3252
|
);
|
|
2523
3253
|
}
|
|
2524
|
-
function
|
|
3254
|
+
function md2(value) {
|
|
2525
3255
|
return String(value ?? "Unknown").replace(
|
|
2526
3256
|
/[&<>"'\\`*_[\]{}()|!#~\r\n]/g,
|
|
2527
3257
|
(char) => char === "\r" || char === "\n" ? " " : `&#${char.charCodeAt(0)};`
|
|
@@ -2530,7 +3260,7 @@ function md(value) {
|
|
|
2530
3260
|
function number(value) {
|
|
2531
3261
|
return value === null ? "Unknown" : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
2532
3262
|
}
|
|
2533
|
-
function
|
|
3263
|
+
function tables2(report) {
|
|
2534
3264
|
const sessionSource = report.sources.find((source2) => source2.name === "Daily session inventory");
|
|
2535
3265
|
return [
|
|
2536
3266
|
{
|
|
@@ -2660,15 +3390,15 @@ function metrics(report) {
|
|
|
2660
3390
|
}
|
|
2661
3391
|
function renderRuntimeReportMarkdown(report) {
|
|
2662
3392
|
const lines = [
|
|
2663
|
-
`# ${
|
|
3393
|
+
`# ${md2(report.title)}`,
|
|
2664
3394
|
"",
|
|
2665
|
-
`**${
|
|
3395
|
+
`**${md2(report.product)}**`,
|
|
2666
3396
|
"",
|
|
2667
|
-
`Assessment: **${
|
|
3397
|
+
`Assessment: **${HEALTH2[report.health]}**`,
|
|
2668
3398
|
"",
|
|
2669
|
-
`Window: ${
|
|
3399
|
+
`Window: ${md2(report.window.start)} \u2192 ${md2(report.window.end)}`,
|
|
2670
3400
|
"",
|
|
2671
|
-
`${
|
|
3401
|
+
`${md2(report.window.description)}. Collection: ${md2(report.collectionStartedAt)} \u2192 ${md2(report.generatedAt)}.`,
|
|
2672
3402
|
"",
|
|
2673
3403
|
"## At a glance",
|
|
2674
3404
|
"",
|
|
@@ -2684,17 +3414,17 @@ function renderRuntimeReportMarkdown(report) {
|
|
|
2684
3414
|
);
|
|
2685
3415
|
for (const finding of report.findings)
|
|
2686
3416
|
lines.push(
|
|
2687
|
-
`### ${finding.priority.toUpperCase()}: ${
|
|
3417
|
+
`### ${finding.priority.toUpperCase()}: ${md2(finding.title)}`,
|
|
2688
3418
|
"",
|
|
2689
|
-
`Evidence: ${
|
|
3419
|
+
`Evidence: ${md2(finding.evidence)}`,
|
|
2690
3420
|
"",
|
|
2691
|
-
`Next step: ${
|
|
3421
|
+
`Next step: ${md2(finding.recommendation)}`,
|
|
2692
3422
|
"",
|
|
2693
|
-
`Source: ${
|
|
3423
|
+
`Source: ${md2(finding.source)}`,
|
|
2694
3424
|
""
|
|
2695
3425
|
);
|
|
2696
|
-
for (const table of
|
|
2697
|
-
lines.push(`## ${table.title}`, "",
|
|
3426
|
+
for (const table of tables2(report)) {
|
|
3427
|
+
lines.push(`## ${table.title}`, "", md2(table.note), "");
|
|
2698
3428
|
if (table.rows.length === 0)
|
|
2699
3429
|
lines.push(
|
|
2700
3430
|
"No rows available. Consult the source status before interpreting this as an empty result.",
|
|
@@ -2704,21 +3434,21 @@ function renderRuntimeReportMarkdown(report) {
|
|
|
2704
3434
|
lines.push(
|
|
2705
3435
|
`| ${table.headers.join(" | ")} |`,
|
|
2706
3436
|
`| ${table.headers.map(() => "---").join(" | ")} |`,
|
|
2707
|
-
...table.rows.map((row) => `| ${row.map(
|
|
3437
|
+
...table.rows.map((row) => `| ${row.map(md2).join(" | ")} |`),
|
|
2708
3438
|
""
|
|
2709
3439
|
);
|
|
2710
3440
|
}
|
|
2711
3441
|
lines.push(
|
|
2712
3442
|
"## Scope, privacy, and limitations",
|
|
2713
3443
|
"",
|
|
2714
|
-
...report.limitations.map((note) => `- ${
|
|
3444
|
+
...report.limitations.map((note) => `- ${md2(note)}`),
|
|
2715
3445
|
"",
|
|
2716
3446
|
`Generated by Prisma AIRS CLI \xB7 Report schema ${report.schemaVersion}`,
|
|
2717
3447
|
""
|
|
2718
3448
|
);
|
|
2719
3449
|
return lines.join("\n");
|
|
2720
3450
|
}
|
|
2721
|
-
var
|
|
3451
|
+
var CSS2 = `
|
|
2722
3452
|
#evidence td:nth-child(3){white-space:nowrap}
|
|
2723
3453
|
:root{color-scheme:light;--ink:#172b40;--muted:#526476;--line:#d8e2e9;--bg:#f2f5f8;--accent:#00786e;font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
2724
3454
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink)}a{color:var(--accent)}header{background:#11283d;color:#fff;padding:2.5rem max(1.25rem,calc((100vw - 1240px)/2))}header p{color:#d7e4ec;max-width:85ch}h1{font-size:clamp(1.8rem,4vw,2.7rem);line-height:1.15;margin:.6rem 0 1rem;overflow-wrap:anywhere}h2{font-size:1.35rem;margin:0 0 .6rem}h3{font-size:1.05rem;margin:.35rem 0}p{margin:.5rem 0 1rem}.eyebrow{font-size:.8rem;letter-spacing:.12em;text-transform:uppercase}.badge{display:inline-block;padding:.35rem .7rem;border:1px solid currentColor;border-radius:100px;font-weight:650;background:#fff;color:var(--ink)}.attention .badge{color:#a62b1b}.review .badge{color:#795500}.unknown .badge{color:#69439b}.no-findings .badge{color:#006a54}
|
|
@@ -2726,7 +3456,7 @@ main{max-width:1280px;margin:auto;padding:1.5rem 1.25rem 3rem}nav{display:flex;g
|
|
|
2726
3456
|
@media(max-width:760px){.cards{grid-template-columns:repeat(2,minmax(0,1fr))}section{padding:1rem}header{padding:1.5rem 1.25rem}.card strong{font-size:1.5rem}}
|
|
2727
3457
|
@media print{body{background:#fff;font-size:10pt}header{background:#fff;color:#000;padding:0}header p{color:#333}main{padding:0;max-width:none}nav,.toolbar,.skip{display:none!important}section,.card{border-radius:0;break-inside:auto}.finding,.card{break-inside:avoid}.table-wrap{overflow:visible}table{font-size:8pt}th,td{min-width:0;padding:.3rem}.hidden,[hidden]{display:revert!important}h2,h3{break-after:avoid}a{color:#000}}
|
|
2728
3458
|
`;
|
|
2729
|
-
var
|
|
3459
|
+
var JS2 = `
|
|
2730
3460
|
'use strict';
|
|
2731
3461
|
document.documentElement.classList.add('js-ready');
|
|
2732
3462
|
document.getElementById('print-report').addEventListener('click',function(){window.print();});
|
|
@@ -2744,21 +3474,21 @@ search.addEventListener('input',function(){
|
|
|
2744
3474
|
});
|
|
2745
3475
|
`;
|
|
2746
3476
|
function renderRuntimeReportHtml(report) {
|
|
2747
|
-
const hash = (text) =>
|
|
2748
|
-
const policy = `default-src 'none'; script-src 'sha256-${hash(
|
|
3477
|
+
const hash = (text) => createHash2("sha256").update(text).digest("base64");
|
|
3478
|
+
const policy = `default-src 'none'; script-src 'sha256-${hash(JS2)}'; style-src 'sha256-${hash(CSS2)}'; base-uri 'none'; form-action 'none'; connect-src 'none'; img-src 'none'`;
|
|
2749
3479
|
const findingCards = report.findings.map(
|
|
2750
|
-
(finding) => `<article class="finding" data-priority="${
|
|
3480
|
+
(finding) => `<article class="finding" data-priority="${html2(finding.priority)}"><span class="priority">${html2(finding.priority)}</span><h3>${html2(finding.title)}</h3><p><strong>Evidence:</strong> ${html2(finding.evidence)}</p><p><strong>Next step:</strong> ${html2(finding.recommendation)}</p><p class="note">Source: ${html2(finding.source)}</p></article>`
|
|
2751
3481
|
).join("");
|
|
2752
|
-
const sections =
|
|
2753
|
-
(table) => `<section id="${table.id}" aria-labelledby="${table.id}-title"><h2 id="${table.id}-title">${table.title}</h2><p class="note">${
|
|
3482
|
+
const sections = tables2(report).map(
|
|
3483
|
+
(table) => `<section id="${table.id}" aria-labelledby="${table.id}-title"><h2 id="${table.id}-title">${table.title}</h2><p class="note">${html2(table.note)}</p>${table.id === "applications" ? '<div class="toolbar js-only"><label for="app-search">Find an application</label><input id="app-search" type="search" placeholder="Filter application buckets"><span id="app-count" role="status"></span></div>' : ""}${table.rows.length ? `<div class="table-wrap" role="region" aria-label="${table.title}" tabindex="0"><table><caption>${table.rows.length} collected rows</caption><thead><tr>${table.headers.map((header) => `<th scope="col">${header}</th>`).join("")}</tr></thead><tbody>${table.rows.map((row) => `<tr>${row.map((cell) => `<td>${html2(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>` : "<p>No rows available. Consult the source status before interpreting this as an empty result.</p>"}</section>`
|
|
2754
3484
|
).join("");
|
|
2755
3485
|
return `<!doctype html>
|
|
2756
|
-
<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="${
|
|
2757
|
-
<body class="${
|
|
3486
|
+
<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="${html2(policy)}"><meta name="referrer" content="no-referrer"><title>${html2(report.title)} \u2014 Prisma AIRS</title><style>${CSS2}</style></head>
|
|
3487
|
+
<body class="${html2(report.health)}"><a class="skip" href="#main">Skip to report</a><header><div class="eyebrow">Prisma AIRS \xB7 AI Runtime Security</div><h1>${html2(report.title)}</h1><span class="badge">${HEALTH2[report.health]}</span><p>${html2(report.window.description)}<br><time>${html2(report.window.start)}</time> \u2192 <time>${html2(report.window.end)}</time></p><p>Collected ${html2(report.collectionStartedAt)} \u2192 ${html2(report.generatedAt)}<br>Read-only evidence \xB7 Confidential environment metadata</p></header>
|
|
2758
3488
|
<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>
|
|
2759
3489
|
<div class="cards">${metrics(report).map(([name, value]) => `<div class="card"><span>${name}</span><strong>${value}</strong></div>`).join("")}</div>
|
|
2760
3490
|
<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>
|
|
2761
|
-
${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${
|
|
3491
|
+
${sections}<section id="scope"><h2>Scope, privacy, and limitations</h2><ul>${report.limitations.map((note) => `<li>${html2(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>${JS2}</script></body></html>
|
|
2762
3492
|
`;
|
|
2763
3493
|
}
|
|
2764
3494
|
|
|
@@ -2778,10 +3508,10 @@ function safeFailure(error) {
|
|
|
2778
3508
|
}
|
|
2779
3509
|
return "Source request or response validation failed; check connectivity and SDK compatibility.";
|
|
2780
3510
|
}
|
|
2781
|
-
function
|
|
3511
|
+
function label2(value) {
|
|
2782
3512
|
return (value?.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim() || "Unknown").slice(0, 240);
|
|
2783
3513
|
}
|
|
2784
|
-
function
|
|
3514
|
+
function count3(value) {
|
|
2785
3515
|
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
2786
3516
|
}
|
|
2787
3517
|
async function collect(source2, maxPages, read, identity) {
|
|
@@ -2831,23 +3561,23 @@ function inventoryNext(next, size, offset) {
|
|
|
2831
3561
|
function total(values) {
|
|
2832
3562
|
if (values.some((value) => value === null)) return null;
|
|
2833
3563
|
const result = values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
2834
|
-
return
|
|
3564
|
+
return count3(result);
|
|
2835
3565
|
}
|
|
2836
3566
|
function distribution(rows) {
|
|
2837
3567
|
const groups = /* @__PURE__ */ new Map();
|
|
2838
3568
|
for (const row of rows) {
|
|
2839
|
-
const name =
|
|
3569
|
+
const name = label2(row.violation_status).toLowerCase();
|
|
2840
3570
|
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
2841
3571
|
}
|
|
2842
3572
|
return [...groups].map(([name, n]) => ({ name, count: n })).sort((a, b) => b.count - a.count);
|
|
2843
3573
|
}
|
|
2844
3574
|
function severity(value) {
|
|
2845
3575
|
return {
|
|
2846
|
-
critical:
|
|
2847
|
-
high:
|
|
2848
|
-
medium:
|
|
2849
|
-
low:
|
|
2850
|
-
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)
|
|
2851
3581
|
};
|
|
2852
3582
|
}
|
|
2853
3583
|
async function collectRuntimeDailyReport(client, options = {}) {
|
|
@@ -2910,7 +3640,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
2910
3640
|
offset
|
|
2911
3641
|
});
|
|
2912
3642
|
const size = page.items?.length ?? 0;
|
|
2913
|
-
const totalItems =
|
|
3643
|
+
const totalItems = count3(page.pagination?.total_items);
|
|
2914
3644
|
if (totalItems !== null) {
|
|
2915
3645
|
if (expectedAppTotal !== void 0 && totalItems !== expectedAppTotal)
|
|
2916
3646
|
throw new ReportDataError(
|
|
@@ -2962,7 +3692,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
2962
3692
|
async (offset) => {
|
|
2963
3693
|
const page = await client.dashboard.sessionsOverview({ ...daily, limit: 25, offset });
|
|
2964
3694
|
const size = page.items?.length ?? 0;
|
|
2965
|
-
const totalItems =
|
|
3695
|
+
const totalItems = count3(page.pagination?.total_items);
|
|
2966
3696
|
if (totalItems === null || page.pagination?.skip !== offset || page.pagination?.limit !== 25)
|
|
2967
3697
|
throw new ReportDataError("Missing or mismatched session pagination metadata.");
|
|
2968
3698
|
if (expectedSessionTotal !== void 0 && expectedSessionTotal !== totalItems)
|
|
@@ -3018,9 +3748,9 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3018
3748
|
}
|
|
3019
3749
|
const appIds = new Set(registered.map((app) => app.customer_appId));
|
|
3020
3750
|
const reportApps = applications.map((app) => ({
|
|
3021
|
-
name:
|
|
3022
|
-
sessions:
|
|
3023
|
-
violatingSessions:
|
|
3751
|
+
name: label2(app.name),
|
|
3752
|
+
sessions: count3(app.sessions_total),
|
|
3753
|
+
violatingSessions: count3(app.sessions_violated),
|
|
3024
3754
|
registered: app.id && appIds.has(app.id) ? true : appSource.status === "complete" && app.id ? false : null
|
|
3025
3755
|
})).sort(
|
|
3026
3756
|
(a, b) => (b.violatingSessions ?? -1) - (a.violatingSessions ?? -1) || a.name.localeCompare(b.name)
|
|
@@ -3066,9 +3796,9 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3066
3796
|
(p) => p["model-configuration"] ? [p["model-configuration"]] : []
|
|
3067
3797
|
);
|
|
3068
3798
|
const timeoutActions = [
|
|
3069
|
-
...new Set(configs.map((c) =>
|
|
3799
|
+
...new Set(configs.map((c) => label2(c.latency?.["inline-timeout-action"])))
|
|
3070
3800
|
];
|
|
3071
|
-
const name =
|
|
3801
|
+
const name = label2(profile.profile_name);
|
|
3072
3802
|
if (profile.active === false)
|
|
3073
3803
|
add(
|
|
3074
3804
|
"review",
|
|
@@ -3097,7 +3827,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3097
3827
|
);
|
|
3098
3828
|
return {
|
|
3099
3829
|
name,
|
|
3100
|
-
revision:
|
|
3830
|
+
revision: count3(profile.revision),
|
|
3101
3831
|
active: profile.active ?? null,
|
|
3102
3832
|
modified: profile.last_modified_ts && Number.isFinite(Date.parse(profile.last_modified_ts)) ? new Date(profile.last_modified_ts).toISOString() : null,
|
|
3103
3833
|
timeoutActions,
|
|
@@ -3145,8 +3875,8 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3145
3875
|
"Review these sessions in SCM and check integration error handling. This is not a tenant-wide failure rate.",
|
|
3146
3876
|
sessionSource.name
|
|
3147
3877
|
);
|
|
3148
|
-
const chartSessions = chartSource.status === "unavailable" ? null : total(chart.map((bucket) =>
|
|
3149
|
-
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)));
|
|
3150
3880
|
if (chartSessions !== null && sessionSource.status === "complete" && chartSessions !== rawSessions.length)
|
|
3151
3881
|
add(
|
|
3152
3882
|
"review",
|
|
@@ -3157,7 +3887,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3157
3887
|
);
|
|
3158
3888
|
const urgentViolations = total(
|
|
3159
3889
|
trend.map(
|
|
3160
|
-
(bucket) => total([
|
|
3890
|
+
(bucket) => total([count3(bucket.violation_breakdown.critical), count3(bucket.violation_breakdown.high)])
|
|
3161
3891
|
)
|
|
3162
3892
|
);
|
|
3163
3893
|
if ((urgentViolations ?? 0) > 0)
|
|
@@ -3173,7 +3903,7 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3173
3903
|
return {
|
|
3174
3904
|
schemaVersion: 2,
|
|
3175
3905
|
product: "Prisma AIRS AI Runtime Security",
|
|
3176
|
-
title:
|
|
3906
|
+
title: label2(options.title ?? "Daily environment report"),
|
|
3177
3907
|
generatedAt: now().toISOString(),
|
|
3178
3908
|
collectionStartedAt: end,
|
|
3179
3909
|
window: {
|
|
@@ -3192,10 +3922,10 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3192
3922
|
},
|
|
3193
3923
|
profiles,
|
|
3194
3924
|
registeredApps: registered.map((app) => ({
|
|
3195
|
-
name:
|
|
3196
|
-
environment:
|
|
3197
|
-
cloud:
|
|
3198
|
-
model:
|
|
3925
|
+
name: label2(app.app_name),
|
|
3926
|
+
environment: label2(app.environment),
|
|
3927
|
+
cloud: label2(app.cloud_provider),
|
|
3928
|
+
model: label2(app.model_name),
|
|
3199
3929
|
keyAssociations: app.api_keys_dp_info ? app.api_keys_dp_info.length : null
|
|
3200
3930
|
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
3201
3931
|
sessions: {
|
|
@@ -3210,22 +3940,22 @@ async function collectRuntimeDailyReport(client, options = {}) {
|
|
|
3210
3940
|
sessions: chartSessions,
|
|
3211
3941
|
violatingSessions: chartViolatingSessions,
|
|
3212
3942
|
buckets: chart.map((bucket) => ({
|
|
3213
|
-
time:
|
|
3214
|
-
sessions:
|
|
3215
|
-
violatingSessions:
|
|
3943
|
+
time: label2(bucket.time),
|
|
3944
|
+
sessions: count3(bucket.total),
|
|
3945
|
+
violatingSessions: count3(bucket.violating),
|
|
3216
3946
|
violations: severity(bucket.violation_breakdown)
|
|
3217
3947
|
}))
|
|
3218
3948
|
},
|
|
3219
3949
|
topApplications: rankings.map((app) => ({
|
|
3220
|
-
name:
|
|
3221
|
-
violations:
|
|
3950
|
+
name: label2(app.name),
|
|
3951
|
+
violations: count3(app.total_violations),
|
|
3222
3952
|
detectors: app.policy_violations.map((detector) => ({
|
|
3223
|
-
name:
|
|
3224
|
-
count:
|
|
3953
|
+
name: label2(detector.detection_type),
|
|
3954
|
+
count: count3(detector.total)
|
|
3225
3955
|
}))
|
|
3226
3956
|
})),
|
|
3227
3957
|
violationTrend: trend.map((bucket) => ({
|
|
3228
|
-
time:
|
|
3958
|
+
time: label2(bucket.date),
|
|
3229
3959
|
violations: severity(bucket.violation_breakdown)
|
|
3230
3960
|
}))
|
|
3231
3961
|
},
|
|
@@ -3267,7 +3997,13 @@ export {
|
|
|
3267
3997
|
validateTopic,
|
|
3268
3998
|
computeMetrics,
|
|
3269
3999
|
computeCategoryBreakdown,
|
|
4000
|
+
collectGatewayEnvironmentReport,
|
|
4001
|
+
renderEnvironmentReportMarkdown,
|
|
4002
|
+
renderEnvironmentReportHtml,
|
|
3270
4003
|
writeReportFile,
|
|
4004
|
+
collectRedTeamEnvironmentReport,
|
|
4005
|
+
renderRedTeamReportMarkdown,
|
|
4006
|
+
renderRedTeamReportHtml,
|
|
3271
4007
|
renderRuntimeReportMarkdown,
|
|
3272
4008
|
renderRuntimeReportHtml,
|
|
3273
4009
|
collectRuntimeDailyReport
|